dsh-packycode-api 1.1.9 → 1.2.0

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
@@ -39,6 +39,39 @@ window.__ModuleLoader__.load({
39
39
  nav: "PackyCode",
40
40
  intro: "填入 PackyCode(PackyAPI)的 API Key 即可使用内置模型路由。多个 Key 按顺序使用:额度用尽或失效时自动切到下一个;某个 Key 的分组用不了所选模型时,自动改用分组合适的 Key。",
41
41
  accountsTitle: "账户与 Key",
42
+ imagesTitle: "图片生成与编辑",
43
+ imagesSeparateKey: "独立图片 Key",
44
+ imagesBalanceLoading: "正在读取图片 Key 余额…",
45
+ imagesBalanceFailed: "图片余额读取失败",
46
+ imagesBalanceHint: "仅汇总图片 Key 的额度,每分钟自动更新;也可手动刷新。与对话 Key 的余额分开统计。",
47
+ imagesIntro: "使用独立图片 Key 调用 PackyCode 图片接口。生成新图或上传图片后描述修改要求;图片模型独立于对话模型。原始 PNG 保存在本机,聊天中显示图片预览。",
48
+ imagesEnabled: "图片生成与编辑",
49
+ imagesOn: "启用",
50
+ imagesOff: "停用",
51
+ imagesModel: "请求型号",
52
+ imagesQuality: "请求质量",
53
+ imagesQuality_auto: "自动",
54
+ imagesQuality_low: "低",
55
+ imagesQuality_medium: "中",
56
+ imagesQuality_high: "高",
57
+ imagesQuality_xhigh: "超高(xhigh)",
58
+ imagesQuality_max: "最高(max)",
59
+ imagesQualityHint: "2.5 支持额外的 xhigh / max;质量越高,耗时和费用通常越高。网关实际支持情况需用图片 Key 验证。不会自动换型号或降低质量。",
60
+ imagesAddKey: "添加图片 Key",
61
+ imagesKeyPlaceholder: "粘贴专用的图片 API Key",
62
+ imagesNoKey: "尚未添加图片 Key。添加后可使用图片工具;对话 Key 不会用于图片请求。",
63
+ imagesLoadFailed: "图片设置读取失败。若刚更新本地插件,请重新加载插件后再打开设置。",
64
+ imagesUnavailable: "当前 Host 未提供图片附件服务,图片工具暂不可用。",
65
+ imagesCreate: "创建图片",
66
+ imagesShortcutHint: "在草稿中加入 PackyCode 生图指令,再补充图片描述后发送。",
67
+ imagesDraft: "请使用 packycode_image_generate 工具,跟随 PackyCode 已保存的图片型号和质量。有参考图时编辑参考图,没有参考图时生成新图。图片要求:",
68
+ imagesDraftBusy: "输入框正忙或已有引用,请直接在草稿里描述图片要求。",
69
+ imagesGenerating: "正在生成…",
70
+ imagesFailed: "生成失败",
71
+ imagesDone: "已生成",
72
+ imagesOpen: "打开图片",
73
+ imagesRetryPreview: "预览加载失败,点击重试",
74
+ imagesLoadingPreview: "正在加载图片…",
42
75
  refresh: "刷新",
43
76
  refreshing: "刷新中…",
44
77
  totalRemaining: "合计剩余",
@@ -68,6 +101,7 @@ window.__ModuleLoader__.load({
68
101
  resetMonthly: "每月重置",
69
102
  resetWeekly: "每周重置",
70
103
  resetDaily: "每日重置",
104
+ resetNever: "不重置",
71
105
  loadFailed: "读取失败",
72
106
  removeAll: "清空全部",
73
107
  addTitle: "添加 Key",
@@ -180,6 +214,39 @@ window.__ModuleLoader__.load({
180
214
  modelsEmpty: "No models selected. PackyCode models are hidden from the picker.",
181
215
  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.",
182
216
  accountsTitle: "Accounts and keys",
217
+ imagesTitle: "Image generation and editing",
218
+ imagesSeparateKey: "Separate image key",
219
+ imagesBalanceLoading: "Reading image key balances…",
220
+ imagesBalanceFailed: "Could not read image balances",
221
+ imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
222
+ 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
+ imagesEnabled: "Generate and edit images",
224
+ imagesOn: "Enabled",
225
+ imagesOff: "Disabled",
226
+ imagesModel: "Image model",
227
+ imagesQuality: "Request quality",
228
+ imagesQuality_auto: "Auto",
229
+ imagesQuality_low: "Low",
230
+ imagesQuality_medium: "Medium",
231
+ imagesQuality_high: "High",
232
+ imagesQuality_xhigh: "Extra high (xhigh)",
233
+ imagesQuality_max: "Maximum (max)",
234
+ imagesQualityHint: "The 2.5 engines also support xhigh / max. Higher quality usually takes longer and costs more. Gateway support needs testing with an image key. Models and quality are never changed automatically.",
235
+ imagesAddKey: "Add an image key",
236
+ imagesKeyPlaceholder: "Paste the dedicated image API key",
237
+ imagesNoKey: "No image key yet. Add one to enable the image tool. Conversation keys are never used for images.",
238
+ imagesLoadFailed: "Could not read image settings. If you just updated the local plugin, reload it and reopen settings.",
239
+ imagesUnavailable: "This host does not provide image attachments; the image tool is unavailable.",
240
+ imagesCreate: "Create image",
241
+ imagesShortcutHint: "Add a PackyCode image instruction to the draft, describe the image, then send.",
242
+ imagesDraft: "Use packycode_image_generate with the saved PackyCode image model and quality. Edit the attached references when provided; otherwise generate a new image. Image requirements:",
243
+ imagesDraftBusy: "The composer is busy or contains references. Describe the image requirements directly in the draft.",
244
+ imagesGenerating: "Generating…",
245
+ imagesFailed: "Generation failed",
246
+ imagesDone: "Generated",
247
+ imagesOpen: "Open image",
248
+ imagesRetryPreview: "Preview failed; click to retry",
249
+ imagesLoadingPreview: "Loading image…",
183
250
  refresh: "Refresh",
184
251
  refreshing: "Refreshing…",
185
252
  totalRemaining: "Total remaining",
@@ -209,6 +276,7 @@ window.__ModuleLoader__.load({
209
276
  resetMonthly: "resets monthly",
210
277
  resetWeekly: "resets weekly",
211
278
  resetDaily: "resets daily",
279
+ resetNever: "does not reset",
212
280
  loadFailed: "Read failed",
213
281
  removeAll: "Remove all",
214
282
  addTitle: "Add a key",
@@ -339,6 +407,11 @@ window.__ModuleLoader__.load({
339
407
  .pkc-input:focus{border-color:var(--dsw-alias-state-business-primary)}
340
408
  .pkc-input::placeholder{color:var(--dsw-alias-label-caption)}
341
409
  .pkc-input:disabled{opacity:.5}
410
+ .pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
411
+ .pkc-image-shortcut{display:inline-flex;align-items:center;gap:8px}
412
+ .pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
413
+ .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
+ .pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
342
415
  .pkc-form{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
343
416
 
344
417
  .pkc-chip{flex:none;padding:0 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:18px;white-space:nowrap}
@@ -416,6 +489,46 @@ window.__ModuleLoader__.load({
416
489
  .pkc-setting>.pkc-label{padding-top:0}
417
490
  }
418
491
  `;
492
+ //#endregion
493
+ //#region src/image-conversation.js
494
+ /** Project this plugin's durable image results into chat without duplicating context. */
495
+ const IMAGE_CHAT_KIND = "packycode-image-output";
496
+ const successfulImages = (event) => event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image" ? (event.data.message?.content ?? []).filter((block) => block.type === "tool-result" && !block.isError && block.content?.some((part) => part.type === "image" && part.attachment)) : [];
497
+ const imageConversationNode = {
498
+ kind: IMAGE_CHAT_KIND,
499
+ target: "chat",
500
+ match(event) {
501
+ return event.type === "turn/end" || successfulImages(event).length > 0 ? {
502
+ id: String(event.data.turn),
503
+ role: "update"
504
+ } : null;
505
+ },
506
+ start: () => void 0,
507
+ update: (context) => context.state,
508
+ buildViewNode(context) {
509
+ const finished = context.matches.find((match) => match.event.type === "turn/end");
510
+ if (finished === void 0) return null;
511
+ const results = context.matches.filter((match) => successfulImages(match.event).length > 0);
512
+ if (results.length === 0) return null;
513
+ const blocks = results.flatMap(({ event }) => successfulImages(event).map((block) => ({
514
+ ...block,
515
+ kind: "tool-result",
516
+ meta: event.data.meta
517
+ })));
518
+ const answerSeq = finished.location.turn?.steps?.at(-1)?.data.get("assistant-step")?.finalNode?.seq;
519
+ const lastImageSeq = Math.max(...results.map((match) => match.event.seq));
520
+ return {
521
+ key: context.key,
522
+ id: context.id,
523
+ kind: IMAGE_CHAT_KIND,
524
+ target: "chat",
525
+ location: finished.location,
526
+ visibility: "visible",
527
+ anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished.event.seq - .025,
528
+ data: { blocks }
529
+ };
530
+ }
531
+ };
419
532
  //#endregion
420
533
  //#region src/totals.js
421
534
  /**
@@ -468,6 +581,584 @@ window.__ModuleLoader__.load({
468
581
  return total;
469
582
  }
470
583
  //#endregion
584
+ //#region src/client-balance.jsx
585
+ const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
586
+ function formatUsd(value) {
587
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
588
+ try {
589
+ return new Intl.NumberFormat(void 0, {
590
+ style: "currency",
591
+ currency: "USD"
592
+ }).format(value);
593
+ } catch {
594
+ return `$${value.toFixed(2)}`;
595
+ }
596
+ }
597
+ function readTime(ms) {
598
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
599
+ try {
600
+ return new Date(ms).toLocaleTimeString(void 0, {
601
+ hour: "2-digit",
602
+ minute: "2-digit"
603
+ });
604
+ } catch {
605
+ return new Date(ms).toISOString().slice(11, 16);
606
+ }
607
+ }
608
+ const RESET_KEYS = {
609
+ monthly: "resetMonthly",
610
+ weekly: "resetWeekly",
611
+ daily: "resetDaily",
612
+ never: "resetNever"
613
+ };
614
+ /** The same quota presentation for conversation and image credential records. */
615
+ function KeyBalance({ row, t }) {
616
+ const usage = row?.usage;
617
+ const figures = [
618
+ usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
619
+ usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
620
+ usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
621
+ ].filter(Boolean);
622
+ 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(" · ");
623
+ 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;
624
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
626
+ className: "pkc-meta",
627
+ children: meta
628
+ }),
629
+ figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
630
+ className: "pkc-figures",
631
+ children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
632
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
633
+ className: "pkc-meta",
634
+ children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
635
+ }),
636
+ share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
637
+ className: "pkc-bar",
638
+ "data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
639
+ role: "presentation",
640
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
641
+ }),
642
+ row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
643
+ className: "pkc-warn",
644
+ children: fill(t("staleNote"), {
645
+ time: readTime(row.stale.fetchedAt),
646
+ error: row.stale.error ?? ""
647
+ })
648
+ })
649
+ ] });
650
+ }
651
+ /** Only this card's keys contribute; account-wide billing is never added. */
652
+ function BalanceTotals({ rows, t }) {
653
+ const totals = sumBalances(rows);
654
+ const stale = rows.filter((row) => row.stale !== void 0).length;
655
+ const known = (field) => rows.some((row) => row.usage?.unlimitedQuota !== true && Number.isFinite(row.usage?.[field]));
656
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
657
+ className: "pkc-stats",
658
+ children: [
659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
660
+ className: "pkc-stat",
661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
662
+ className: "pkc-stat-label",
663
+ children: t("totalRemaining")
664
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
665
+ className: "pkc-stat-value",
666
+ children: [known("availableUsd") ? formatUsd(totals.remainingUsd) : totals.unlimited > 0 ? t("keyUnlimited") : "—", known("availableUsd") && totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
667
+ })]
668
+ }),
669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
670
+ className: "pkc-stat",
671
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
672
+ className: "pkc-stat-label",
673
+ children: t("totalUsed")
674
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
675
+ className: "pkc-stat-value",
676
+ children: known("usedUsd") ? formatUsd(totals.usedUsd) : "—"
677
+ })]
678
+ }),
679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
680
+ className: "pkc-stat",
681
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
682
+ className: "pkc-stat-label",
683
+ children: t("totalLimit")
684
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
685
+ className: "pkc-stat-value",
686
+ children: totals.unlimited > 0 ? t("keyUnlimited") : known("limitUsd") ? formatUsd(totals.limitUsd) : "—"
687
+ })]
688
+ })
689
+ ]
690
+ }), (totals.unread > 0 || stale > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
691
+ className: "pkc-note",
692
+ children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, stale > 0 ? fill(t("totalStale"), { count: stale }) : void 0].filter(Boolean).join(" ")
693
+ })] });
694
+ }
695
+ //#endregion
696
+ //#region src/client-images.jsx
697
+ const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
698
+ /** Only projections come back from the host; the password draft is cleared on success. */
699
+ function PackyCodeImagesCard({ call, t }) {
700
+ const [state, setState] = react.useState();
701
+ const [draft, setDraft] = react.useState({
702
+ key: "",
703
+ label: ""
704
+ });
705
+ const [busy, setBusy] = react.useState(false);
706
+ const [error, setError] = react.useState();
707
+ const [balance, setBalance] = react.useState();
708
+ const [balanceError, setBalanceError] = react.useState();
709
+ const [balanceLoading, setBalanceLoading] = react.useState(true);
710
+ const balanceGeneration = react.useRef(0);
711
+ const mounted = react.useRef(false);
712
+ const readBalance = async (force = false) => {
713
+ if (!mounted.current) return;
714
+ const generation = ++balanceGeneration.current;
715
+ setBalanceLoading(true);
716
+ setBalanceError(void 0);
717
+ try {
718
+ const value = await call("images/balance", { force });
719
+ if (mounted.current && balanceGeneration.current === generation) setBalance(value);
720
+ } catch (failure) {
721
+ if (mounted.current && balanceGeneration.current === generation) setBalanceError(String(failure?.message ?? t("imagesBalanceFailed")).slice(0, 200));
722
+ } finally {
723
+ if (mounted.current && balanceGeneration.current === generation) setBalanceLoading(false);
724
+ }
725
+ };
726
+ react.useEffect(() => {
727
+ mounted.current = true;
728
+ call("images/status").then((value) => {
729
+ if (mounted.current) setState(value);
730
+ }).catch(() => {
731
+ if (mounted.current) setError(t("imagesLoadFailed"));
732
+ });
733
+ readBalance();
734
+ const timer = window.setInterval(() => {
735
+ readBalance();
736
+ }, 6e4);
737
+ return () => {
738
+ mounted.current = false;
739
+ balanceGeneration.current += 1;
740
+ window.clearInterval(timer);
741
+ };
742
+ }, [call, t]);
743
+ const mutate = async (endpoint, payload = {}) => {
744
+ setBusy(true);
745
+ setError(void 0);
746
+ try {
747
+ const value = await call(endpoint, payload);
748
+ if (mounted.current) setState(value);
749
+ if (mounted.current && endpoint.startsWith("images/keys/")) {
750
+ setBalance(void 0);
751
+ readBalance(true);
752
+ }
753
+ window.dispatchEvent(new Event("packycode-images-updated"));
754
+ return true;
755
+ } catch (failure) {
756
+ if (mounted.current) setError(String(failure?.message ?? t("imagesLoadFailed")).slice(0, 200));
757
+ return false;
758
+ } finally {
759
+ if (mounted.current) setBusy(false);
760
+ }
761
+ };
762
+ const disabled = busy || state?.writable !== true;
763
+ const models = state?.models ?? [];
764
+ const qualities = models.find((model) => model.id === state?.imageModel)?.qualities ?? [];
765
+ const rows = (state?.keys ?? []).map((entry) => balance?.keys?.find((row) => row.id === entry.id) ?? { id: entry.id });
766
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
767
+ className: "pkc-card",
768
+ children: [
769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
770
+ className: "pkc-head",
771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
772
+ className: "pkc-title",
773
+ children: t("imagesTitle")
774
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
775
+ className: "pkc-actions",
776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
777
+ className: "pkc-chip",
778
+ "data-tone": "neutral",
779
+ children: t("imagesSeparateKey")
780
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
781
+ className: "pkc-btn",
782
+ type: "button",
783
+ disabled: busy || balanceLoading,
784
+ onClick: () => {
785
+ readBalance(true);
786
+ },
787
+ children: balanceLoading ? t("refreshing") : t("refresh")
788
+ })]
789
+ })]
790
+ }),
791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
792
+ className: "pkc-note",
793
+ children: t("imagesIntro")
794
+ }),
795
+ state !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
797
+ className: "pkc-setting",
798
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
799
+ className: "pkc-label",
800
+ children: t("imagesEnabled")
801
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
802
+ className: "pkc-input pkc-image-choice",
803
+ value: String(state.imageEnabled),
804
+ disabled,
805
+ onChange: (event) => {
806
+ mutate("images/update", { imageEnabled: event.target.value === "true" });
807
+ },
808
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
809
+ value: "true",
810
+ children: t("imagesOn")
811
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
812
+ value: "false",
813
+ children: t("imagesOff")
814
+ })]
815
+ })]
816
+ }),
817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
818
+ className: "pkc-setting",
819
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
820
+ className: "pkc-label",
821
+ children: t("imagesModel")
822
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
823
+ className: "pkc-input pkc-image-choice",
824
+ value: state.imageModel,
825
+ disabled: disabled || !state.imageEnabled,
826
+ onChange: (event) => {
827
+ mutate("images/update", {
828
+ imageModel: event.target.value,
829
+ imageQuality: "auto"
830
+ });
831
+ },
832
+ children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
833
+ value: model.id,
834
+ children: imageModelLabel(model.id)
835
+ }, model.id))
836
+ })]
837
+ }),
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
839
+ className: "pkc-setting",
840
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
841
+ className: "pkc-label",
842
+ children: t("imagesQuality")
843
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
844
+ className: "pkc-input pkc-image-choice",
845
+ value: state.imageQuality,
846
+ disabled: disabled || !state.imageEnabled,
847
+ onChange: (event) => {
848
+ mutate("images/update", { imageQuality: event.target.value });
849
+ },
850
+ children: qualities.map((quality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
851
+ value: quality,
852
+ children: t(`imagesQuality_${quality}`)
853
+ }, quality))
854
+ })]
855
+ }),
856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
857
+ className: "pkc-note",
858
+ children: t("imagesQualityHint")
859
+ }),
860
+ (state.keys ?? []).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
861
+ rows,
862
+ t
863
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
864
+ className: "pkc-note",
865
+ children: balanceLoading ? t("imagesBalanceLoading") : t("imagesBalanceHint")
866
+ })] }),
867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
868
+ className: "pkc-list",
869
+ children: (state.keys ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
870
+ className: "pkc-row",
871
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
872
+ className: "pkc-row-top",
873
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
874
+ className: "pkc-row-name",
875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
876
+ className: "pkc-name",
877
+ children: entry.label
878
+ }), entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
879
+ className: "pkc-chip",
880
+ "data-tone": "success",
881
+ children: t("keyActive")
882
+ })]
883
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
884
+ className: "pkc-actions",
885
+ children: [!entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
886
+ className: "pkc-btn",
887
+ type: "button",
888
+ disabled: busy,
889
+ onClick: () => {
890
+ mutate("images/keys/select", { id: entry.id });
891
+ },
892
+ children: t("keyUse")
893
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
894
+ className: "pkc-btn",
895
+ "data-variant": "danger",
896
+ type: "button",
897
+ disabled: busy,
898
+ onClick: () => {
899
+ mutate("images/keys/remove", { id: entry.id });
900
+ },
901
+ children: t("keyRemove")
902
+ })]
903
+ })]
904
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
905
+ row: rows.find((row) => row.id === entry.id),
906
+ t
907
+ })]
908
+ }, entry.id))
909
+ }),
910
+ (state.keys ?? []).length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
911
+ className: "pkc-note",
912
+ children: t("imagesNoKey")
913
+ }),
914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
915
+ className: "pkc-add",
916
+ onSubmit: (event) => {
917
+ event.preventDefault();
918
+ if (busy || draft.key.trim() === "") return;
919
+ mutate("images/keys/add", draft).then((saved) => {
920
+ if (saved && mounted.current) setDraft({
921
+ key: "",
922
+ label: ""
923
+ });
924
+ });
925
+ },
926
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
927
+ className: "pkc-label",
928
+ children: t("imagesAddKey")
929
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
930
+ className: "pkc-form",
931
+ children: [
932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
933
+ className: "pkc-input",
934
+ type: "password",
935
+ autoComplete: "off",
936
+ "aria-label": t("imagesAddKey"),
937
+ placeholder: t("imagesKeyPlaceholder"),
938
+ value: draft.key,
939
+ disabled: busy,
940
+ onChange: (event) => setDraft((current) => ({
941
+ ...current,
942
+ key: event.target.value
943
+ }))
944
+ }),
945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
946
+ className: "pkc-input",
947
+ type: "text",
948
+ maxLength: 60,
949
+ "aria-label": t("addLabelPlaceholder"),
950
+ placeholder: t("addLabelPlaceholder"),
951
+ value: draft.label,
952
+ disabled: busy,
953
+ onChange: (event) => setDraft((current) => ({
954
+ ...current,
955
+ label: event.target.value
956
+ }))
957
+ }),
958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
959
+ className: "pkc-btn",
960
+ "data-variant": "primary",
961
+ "data-size": "md",
962
+ type: "submit",
963
+ disabled: busy || draft.key.trim() === "",
964
+ children: t("add")
965
+ })
966
+ ]
967
+ })]
968
+ }),
969
+ !state.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
970
+ className: "pkc-note",
971
+ children: t("imagesUnavailable")
972
+ }),
973
+ !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
974
+ className: "pkc-note",
975
+ children: t("searchNotWritable")
976
+ })
977
+ ] }),
978
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
979
+ className: "pkc-error",
980
+ role: "alert",
981
+ children: error
982
+ }),
983
+ balanceError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
984
+ className: "pkc-error",
985
+ role: "alert",
986
+ children: `${t("imagesBalanceFailed")}: ${balanceError}`
987
+ })
988
+ ]
989
+ });
990
+ }
991
+ /** A draft shortcut names this provider explicitly when other image tools exist. */
992
+ function PackyCodeImageShortcut({ call, t, directory, prepare }) {
993
+ const active = react.useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot())?.current?.provider === "packycode";
994
+ const [ready, setReady] = react.useState(false);
995
+ const [error, setError] = react.useState();
996
+ react.useEffect(() => {
997
+ let live = true;
998
+ const refresh = () => {
999
+ if (active) call("images/status").then((value) => {
1000
+ if (live) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
1001
+ }).catch(() => {
1002
+ if (live) setReady(false);
1003
+ });
1004
+ };
1005
+ setReady(false);
1006
+ refresh();
1007
+ window.addEventListener("packycode-images-updated", refresh);
1008
+ return () => {
1009
+ live = false;
1010
+ window.removeEventListener("packycode-images-updated", refresh);
1011
+ };
1012
+ }, [call, active]);
1013
+ if (!active || !ready) return null;
1014
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1015
+ className: "pkc-image-shortcut",
1016
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1017
+ className: "pkc-btn",
1018
+ type: "button",
1019
+ title: t("imagesShortcutHint"),
1020
+ onClick: () => {
1021
+ try {
1022
+ prepare(t("imagesDraft"));
1023
+ setError(void 0);
1024
+ } catch {
1025
+ setError(t("imagesDraftBusy"));
1026
+ }
1027
+ },
1028
+ children: t("imagesCreate")
1029
+ }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1030
+ className: "pkc-error",
1031
+ role: "alert",
1032
+ children: error
1033
+ })]
1034
+ });
1035
+ }
1036
+ function GeneratedImage({ attachment, loadImage, openImage, t }) {
1037
+ const [url, setUrl] = react.useState();
1038
+ const [failed, setFailed] = react.useState(false);
1039
+ const [attempt, setAttempt] = react.useState(0);
1040
+ react.useEffect(() => {
1041
+ let live = true;
1042
+ setUrl(void 0);
1043
+ setFailed(false);
1044
+ Promise.resolve().then(() => loadImage(attachment)).then((value) => {
1045
+ if (live) setUrl(value);
1046
+ }, () => {
1047
+ if (live) setFailed(true);
1048
+ });
1049
+ return () => {
1050
+ live = false;
1051
+ };
1052
+ }, [
1053
+ attachment,
1054
+ loadImage,
1055
+ attempt
1056
+ ]);
1057
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1058
+ className: "pkc-generated-image",
1059
+ type: "button",
1060
+ title: t("imagesOpen"),
1061
+ "aria-label": t("imagesOpen"),
1062
+ disabled: !url && !failed,
1063
+ onClick: (event) => {
1064
+ if (failed) {
1065
+ setAttempt((value) => value + 1);
1066
+ return;
1067
+ }
1068
+ openImage(url, attachment, event.currentTarget);
1069
+ },
1070
+ children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1071
+ src: url,
1072
+ alt: attachment.name ?? t("imagesTitle")
1073
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: failed ? t("imagesRetryPreview") : t("imagesLoadingPreview") })
1074
+ });
1075
+ }
1076
+ function PackyCodeImageResult({ block, t, loadImage, openImage }) {
1077
+ const settled = block?.kind === "tool-result";
1078
+ const failed = settled && block.isError === true;
1079
+ const images = settled ? (block.content ?? []).filter((part) => part.type === "image" && part.attachment) : [];
1080
+ const error = failed ? block.content?.find((part) => part.type === "text")?.text : void 0;
1081
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1082
+ className: "pkc-image-result",
1083
+ children: [
1084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1085
+ className: "pkc-status",
1086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("imagesTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1087
+ className: "pkc-meta",
1088
+ children: ["PackyCode · ", t(!settled ? "imagesGenerating" : failed ? "imagesFailed" : "imagesDone")]
1089
+ })]
1090
+ }),
1091
+ images.map((part) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratedImage, {
1092
+ attachment: part.attachment,
1093
+ t,
1094
+ loadImage,
1095
+ openImage
1096
+ }, part.attachment.attachmentId)),
1097
+ typeof block?.meta?.requestedModel === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1098
+ className: "pkc-meta",
1099
+ children: [
1100
+ block.meta.requestedModel,
1101
+ " · ",
1102
+ String(block.meta.requestedQuality ?? "auto"),
1103
+ " · ",
1104
+ block.meta.width,
1105
+ " × ",
1106
+ block.meta.height
1107
+ ]
1108
+ }),
1109
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1110
+ className: "pkc-error",
1111
+ role: "alert",
1112
+ children: error
1113
+ })
1114
+ ]
1115
+ });
1116
+ }
1117
+ function ImageChatOutput({ node, ...props }) {
1118
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1119
+ className: "pkc-image-chat",
1120
+ children: node.data.blocks.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImageResult, {
1121
+ block,
1122
+ ...props
1123
+ }, block.toolCallId))
1124
+ });
1125
+ }
1126
+ function installImagePresentation(ctx, t) {
1127
+ ctx.inject(["uiConversation"], (scope) => {
1128
+ const conversation = scope.get("uiConversation");
1129
+ if (typeof conversation?.events?.register !== "function" || typeof conversation?.imageUrl !== "function") return;
1130
+ const imageProps = (sessionId) => ({
1131
+ t,
1132
+ loadImage: (attachment) => conversation.imageUrl(sessionId, attachment),
1133
+ openImage: (src, attachment, opener) => {
1134
+ if (scope.get("nativeImageViewer")?.open?.({
1135
+ items: [{
1136
+ id: attachment.attachmentId,
1137
+ src,
1138
+ name: attachment.name ?? "packycode-generated.png",
1139
+ width: attachment.width,
1140
+ height: attachment.height
1141
+ }],
1142
+ opener,
1143
+ source: "packycode-generated"
1144
+ }) === true) return;
1145
+ window.open(src, "_blank", "noopener,noreferrer");
1146
+ }
1147
+ });
1148
+ scope.effect(() => conversation.events.register(imageConversationNode), "packycode-api: image results in chat");
1149
+ scope.slots.inject("conversation.chat.node", () => scope.slots.register({
1150
+ name: "conversation.chat.node",
1151
+ key: IMAGE_CHAT_KIND,
1152
+ inject: imageProps
1153
+ }, ImageChatOutput));
1154
+ scope.slots.inject("tool.call.toolview", () => scope.slots.register({
1155
+ name: "tool.call.toolview",
1156
+ key: "packycode_image_generate",
1157
+ inject: imageProps
1158
+ }, PackyCodeImageResult));
1159
+ });
1160
+ }
1161
+ //#endregion
471
1162
  //#region src/client.jsx
472
1163
  /**
473
1164
  * Browser half: the PackyCode settings page and the composer balance chip.
@@ -517,52 +1208,15 @@ window.__ModuleLoader__.load({
517
1208
  });
518
1209
  }
519
1210
  const useCaller = (rpc) => react.useMemo(() => channelCaller(rpc), [rpc]);
520
- /** Substitute `{name}` placeholders in a copy string. */
521
- const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
522
- function formatUsd(value) {
523
- if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
524
- try {
525
- return new Intl.NumberFormat(void 0, {
526
- style: "currency",
527
- currency: "USD"
528
- }).format(value);
529
- } catch {
530
- return `$${value.toFixed(2)}`;
531
- }
532
- }
533
- /** The clock time of an epoch-millisecond reading, for a "last read at" note. */
534
- function readTime(ms) {
535
- if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
536
- try {
537
- return new Date(ms).toLocaleTimeString(void 0, {
538
- hour: "2-digit",
539
- minute: "2-digit"
540
- });
541
- } catch {
542
- return new Date(ms).toISOString().slice(11, 16);
543
- }
544
- }
545
1211
  function clearMessage(error) {
546
1212
  const text = typeof error?.message === "string" && error.message.length > 0 ? error.message : String(error);
547
1213
  return text.length > 200 ? text.slice(0, 200) : text;
548
1214
  }
549
- const RESET_KEYS = {
550
- monthly: "resetMonthly",
551
- weekly: "resetWeekly",
552
- daily: "resetDaily"
553
- };
554
- const resetLabel = (period, t) => RESET_KEYS[period] === void 0 ? void 0 : t(RESET_KEYS[period]);
555
1215
  /** Active key's usage row, preferring the key the ring currently points at. */
556
1216
  function activeUsage(balance) {
557
1217
  if (balance === void 0) return void 0;
558
1218
  return balance.keys?.find((entry) => entry.id === balance.activeId) ?? balance.keys?.[0];
559
1219
  }
560
- /** How much of a key's limit is left, for its bar; undefined without a limit. */
561
- function remainingShare(usage) {
562
- if (usage?.unlimitedQuota === true || typeof usage?.limitUsd !== "number" || usage.limitUsd <= 0) return void 0;
563
- if (typeof usage.availableUsd !== "number") return void 0;
564
- return Math.max(0, Math.min(1, usage.availableUsd / usage.limitUsd));
565
- }
566
1220
  /** Which catalog families a key reaches, from its per-model coverage. */
567
1221
  function reachOf(coverage) {
568
1222
  return MODEL_FAMILIES.flatMap(([name, models]) => {
@@ -661,14 +1315,6 @@ window.__ModuleLoader__.load({
661
1315
  ] });
662
1316
  }
663
1317
  function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, renaming, onRename, onAction, t }) {
664
- const usage = row?.usage;
665
- const share = remainingShare(usage);
666
- const figures = [
667
- usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
668
- usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
669
- usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
670
- ].filter(Boolean);
671
- const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, resetLabel(usage?.resetPeriod, t)].filter(Boolean).join(" · ");
672
1318
  const editing = renaming?.id === entry.id;
673
1319
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
1320
  className: "pkc-row",
@@ -750,29 +1396,9 @@ window.__ModuleLoader__.load({
750
1396
  ]
751
1397
  })]
752
1398
  }),
753
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
754
- className: "pkc-meta",
755
- children: meta
756
- }),
757
- figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
758
- className: "pkc-figures",
759
- children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
760
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
761
- className: "pkc-meta",
762
- children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
763
- }),
764
- share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
765
- className: "pkc-bar",
766
- "data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
767
- role: "presentation",
768
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
769
- }),
770
- row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
771
- className: "pkc-warn",
772
- children: fill(t("staleNote"), {
773
- time: readTime(row.stale.fetchedAt),
774
- error: row.stale.error ?? ""
775
- })
1399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
1400
+ row,
1401
+ t
776
1402
  }),
777
1403
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyGroups, {
778
1404
  group,
@@ -861,8 +1487,6 @@ window.__ModuleLoader__.load({
861
1487
  };
862
1488
  const keys = status?.keys ?? [];
863
1489
  const rows = balance?.keys ?? [];
864
- const totals = sumBalances(rows);
865
- const staleCount = rows.filter((row) => row.stale !== void 0).length;
866
1490
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
867
1491
  title: t("accountsTitle"),
868
1492
  aside: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -883,44 +1507,9 @@ window.__ModuleLoader__.load({
883
1507
  className: "pkc-note",
884
1508
  children: t("empty")
885
1509
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
886
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
887
- className: "pkc-stats",
888
- children: [
889
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
890
- className: "pkc-stat",
891
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
892
- className: "pkc-stat-label",
893
- children: t("totalRemaining")
894
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
895
- className: "pkc-stat-value",
896
- children: [totals.counted === 0 ? "—" : formatUsd(totals.remainingUsd) ?? "—", totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
897
- })]
898
- }),
899
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
900
- className: "pkc-stat",
901
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
902
- className: "pkc-stat-label",
903
- children: t("totalUsed")
904
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
905
- className: "pkc-stat-value",
906
- children: totals.counted === 0 ? "—" : formatUsd(totals.usedUsd) ?? "—"
907
- })]
908
- }),
909
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
910
- className: "pkc-stat",
911
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
912
- className: "pkc-stat-label",
913
- children: t("totalLimit")
914
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
915
- className: "pkc-stat-value",
916
- children: totals.counted === 0 ? "—" : totals.unlimited > 0 ? t("keyUnlimited") : formatUsd(totals.limitUsd) ?? "—"
917
- })]
918
- })
919
- ]
920
- }),
921
- (totals.unread > 0 || staleCount > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
922
- className: "pkc-note",
923
- children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, staleCount > 0 ? fill(t("totalStale"), { count: staleCount }) : void 0].filter(Boolean).join(" ")
1510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
1511
+ rows,
1512
+ t
924
1513
  }),
925
1514
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
926
1515
  className: "pkc-list",
@@ -1584,6 +2173,10 @@ window.__ModuleLoader__.load({
1584
2173
  call,
1585
2174
  t
1586
2175
  }),
2176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImagesCard, {
2177
+ call,
2178
+ t
2179
+ }),
1587
2180
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsCard, {
1588
2181
  call,
1589
2182
  t
@@ -1661,6 +2254,8 @@ window.__ModuleLoader__.load({
1661
2254
  }, "packycode-api: style");
1662
2255
  const connection = ctx.get("connection");
1663
2256
  const t = ctx.locale.bind(NS);
2257
+ installImagePresentation(ctx, t);
2258
+ const call = channelCaller(connection.rpc);
1664
2259
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1665
2260
  name: "settings.section",
1666
2261
  id: "packycode-api",
@@ -1683,6 +2278,29 @@ window.__ModuleLoader__.load({
1683
2278
  directory: ctx.modelDirectories.directoryFor(sessionId).store
1684
2279
  })
1685
2280
  }, PackyCodeComposerQuota));
2281
+ ctx.inject(["sessions", "conversation"], (scope) => {
2282
+ const sessions = scope.get("sessions");
2283
+ const conversation = scope.get("conversation");
2284
+ if (typeof sessions?.scope !== "function" || typeof conversation?.input?.for !== "function") return;
2285
+ scope.slots.inject("conversation.input.left", () => scope.slots.register({
2286
+ name: "conversation.input.left",
2287
+ id: "packycode-image-create",
2288
+ order: 31,
2289
+ inject: (sessionId) => ({
2290
+ call,
2291
+ t,
2292
+ directory: ctx.modelDirectories.directoryFor(sessionId).store,
2293
+ prepare: (text) => {
2294
+ const session = sessions.scope(sessionId);
2295
+ if (session === void 0) throw new Error("Composer unavailable");
2296
+ const input = conversation.input.for(session);
2297
+ const draft = input.state.getSnapshot();
2298
+ if (draft.phase !== "plain" || draft.occurrences?.length) throw new Error("Composer busy");
2299
+ input.setDraft([draft.draft, text].filter(Boolean).join("\n\n"));
2300
+ }
2301
+ })
2302
+ }, PackyCodeImageShortcut));
2303
+ });
1686
2304
  }
1687
2305
  //#endregion
1688
2306
  exports.apply = apply;