dsh-connect-agnes-token-plan 0.5.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/client.js ADDED
@@ -0,0 +1,3921 @@
1
+ var dsh_connect_sensenova_token_plan_client = (function() {
2
+
3
+ //#region \0rolldown/runtime.js
4
+ var __esmMin = (fn, res, err) => () => {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = fn(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
13
+
14
+ //#endregion
15
+ //#region src/client/const.ts
16
+ var NS, SNAPSHOT_PATH, ACCOUNT_PATH, API_KEY_PATH, PROVIDER_PATH, MODELS_PATH, DRAW_PATH, VIDEO_PATH, RACCOON_PATH, AGNESCODE_PATH, AGNES_SIGNUP_URL;
17
+ var init_const = __esmMin((() => {
18
+ NS = "dsh-connect-agnes-token-plan";
19
+ SNAPSHOT_PATH = "/api/dsh-connect-agnes-token-plan/snapshot";
20
+ ACCOUNT_PATH = "/api/dsh-connect-agnes-token-plan/account";
21
+ API_KEY_PATH = "/api/dsh-connect-agnes-token-plan/api-key";
22
+ PROVIDER_PATH = "/api/dsh-connect-agnes-token-plan/provider";
23
+ MODELS_PATH = "/api/dsh-connect-agnes-token-plan/models";
24
+ DRAW_PATH = "/api/dsh-connect-agnes-token-plan/draw";
25
+ VIDEO_PATH = "/api/dsh-connect-agnes-token-plan/video";
26
+ RACCOON_PATH = "/api/dsh-connect-agnes-token-plan/raccoon";
27
+ AGNESCODE_PATH = "/api/dsh-connect-agnes-token-plan/agnescode";
28
+ AGNES_SIGNUP_URL = "https://platform.agnes-ai.cn";
29
+ }));
30
+
31
+ //#endregion
32
+ //#region src/client/i18n.ts
33
+ var zh, en;
34
+ var init_i18n = __esmMin((() => {
35
+ zh = {
36
+ "entry.label": "积分面板",
37
+ "panel.title": "积分面板",
38
+ "panel.back": "返回会话",
39
+ "panel.refresh": "刷新",
40
+ "panel.updated": "更新于 {time}",
41
+ "panel.loading": "加载中…",
42
+ "panel.error": "读取失败:{error}",
43
+ "panel.jwtMissing": "还没有配置控制台账号。",
44
+ "panel.jwtExpired": "控制台令牌已失效。Agnes 不发放 refresh 令牌,Host 会用已保存的账号重新登录一次;若仍失败,请手动重新登录。",
45
+ "panel.configError": "插件配置有误:{error}",
46
+ "panel.consoleTransient": "Agnes 控制台暂时无法读取,通常下一次自动刷新即可恢复;若持续出现,请检查网络后稍再重试。",
47
+ "panel.shapeDrift": "上游返回的结构可能有变:{detail}",
48
+ "auth.title": "连接 Agnes 控制台",
49
+ "auth.registerHint": "还没有账号?前往官网注册,免费开通 Token Plan 额度 →",
50
+ "auth.portalHint": "前往官网管理额度 / 获取 API Key →",
51
+ "auth.username": "邮箱",
52
+ "auth.password": "密码",
53
+ "auth.show": "显示",
54
+ "auth.hide": "隐藏",
55
+ "auth.placeholderUser": "注册 platform.agnes-ai.cn 的邮箱",
56
+ "auth.submit": "登录",
57
+ "auth.submitting": "登录中…",
58
+ "auth.working": "已保存并登录,正在读取额度…",
59
+ "auth.forget": "清除已保存的账号",
60
+ "auth.forgotten": "已清除账号(当前令牌仍可用)",
61
+ "auth.saved": "邮箱与登录令牌已保存在 DSH 凭据中;密码不落盘。Agnes 不发放 refresh 令牌,令牌失效后 Host 会用已保存的邮箱密码重新登录一次。",
62
+ "auth.ephemeral": "注意:当前 Host 没有凭据服务,账号只保存在内存中,重启后需要重新登录。",
63
+ "auth.autoRecoverOn": "自动恢复:已开启,令牌失效后 Host 会用已保存的邮箱密码重新登录",
64
+ "auth.autoRecoverOff": "自动恢复:未开启,令牌失效后需手动重登",
65
+ "auth.badCredentials": "账号或密码不正确",
66
+ "auth.locked": "账号已被锁定。Agnes 会在多次登录失败后锁定账号——请稍后在 Agnes 控制台确认账号状态后再试,期间面板不会自动重试。",
67
+ "auth.retryAfter": "平台要求等待约 {minutes} 分钟后再试;等待期间面板不会自动重试,避免再次触发锁定。",
68
+ "auth.rateLimited": "尝试过于频繁,请稍后再试。",
69
+ "auth.verification": "需要额外验证(短信/图形验证码),自动化登录无法完成,请先在浏览器登录一次。",
70
+ "auth.failed": "登录未完成:{reason}",
71
+ "auth.empty": "请填写账号和密码",
72
+ "auth.network": "无法连接本机 Host",
73
+ "panel.empty": "暂无数据。",
74
+ "section.quota": "额度上限",
75
+ "section.usage": "用量统计(近 {days} 天)",
76
+ "quota.window": "额度",
77
+ "quota.win.requests5h": "模型请求",
78
+ "quota.win.requestsWeekly": "每周请求",
79
+ "quota.win.imagesDaily": "生图",
80
+ "quota.win.videoDaily": "视频",
81
+ "quota.perHours": "{hours} 小时",
82
+ "quota.perDay": "每日",
83
+ "quota.perWeek": "每周",
84
+ "quota.perMonth": " / 月",
85
+ "quota.perYear": " / 年",
86
+ "quota.cycle.monthly": "按月",
87
+ "quota.cycle.yearly": "按年",
88
+ "quota.unit.requests": "次",
89
+ "quota.unit.images": "张",
90
+ "quota.used": "已用",
91
+ "quota.expires": "到期 {time}",
92
+ "quota.none": "暂无额度数据。",
93
+ "quota.planUnknown": "未能识别当前套餐——控制台没有返回可匹配的套餐信息。下方「套餐对比」是平台公开的套餐目录,不依赖登录。",
94
+ "quota.consoleOffline": "Agnes 控制台未连接,本页的额度与用量都读不到。",
95
+ "quota.consoleOfflineHint": "登录入口在本页下方的「{section}」卡片里。",
96
+ "quota.error": "读取 {source} 失败:{message}",
97
+ "quota.catalogue": "套餐对比({count} 档)",
98
+ "quota.windowNote": "Agnes 按窗口限流:上面的上限是「每 N 小时 / 每天 / 每周」的次数。控制台只提供账号累计用量,不提供窗口内的已用量,所以下方累计值与上面的上限周期不同,不能相减。",
99
+ "quota.accountTotals": "账号累计用量(控制台口径)",
100
+ "quota.total.requests": "模型请求",
101
+ "quota.total.tokens": "文本 Token",
102
+ "quota.total.images": "生图",
103
+ "quota.total.video": "视频秒数",
104
+ "quota.total.activeDays": "活跃天数",
105
+ "quota.usageMissing": "{label}:暂未读到。",
106
+ "usage.none": "该区间内没有用量记录。",
107
+ "usage.requests": "模型请求",
108
+ "usage.perBucket": "按平台返回的分桶",
109
+ "usage.legend": "柱高按区间内最高的一桶相对显示,不是占额度上限的比例。",
110
+ "shape.api": "接口",
111
+ "shape.missing": "缺少字段",
112
+ "section.collapse": "收起",
113
+ "section.expand": "展开",
114
+ "quota.vision": "可看图:{models}",
115
+ "quota.visionInferred": "(按模型名推断,平台未声明)",
116
+ "llm.contextBadge": "{ctx} 上下文",
117
+ "auth.selfRenew": "令牌自动续期中",
118
+ "auth.needsLogin": "需要重新登录",
119
+ "llm.title": "API Key",
120
+ "llm.providerTitle": "语言模型",
121
+ "llm.keyField": "API Key",
122
+ "llm.keyEditor": "更换 / 清除 API Key",
123
+ "llm.placeholder": "粘贴 API Key(免费版 sk- 或 Token Plan cpk-)",
124
+ "llm.save": "保存 API Key",
125
+ "llm.saving": "保存中…",
126
+ "llm.forget": "清除已保存的 API Key",
127
+ "llm.saved": "API Key 已保存在 DSH 凭据中;下次轮询自动拉取模型目录。",
128
+ "llm.forgotten": "已清除面板保存的 API Key(环境变量 AGNES_TOKEN_PLAN_API_KEY 不受影响)。",
129
+ "llm.empty": "请输入 API Key",
130
+ "llm.footnote": "Key 只保存在 DSH 凭据中,不会写入插件目录或日志;请求时按次读取。",
131
+ "llm.keyRegisterHint": "还没有 API Key?前往官网免费获取 →",
132
+ "llm.ephemeral": "注意:当前 Host 没有凭据服务,Key 只保存在内存中,重启后失效。",
133
+ "llm.keyPresent": "已配置(来源:{source})",
134
+ "llm.noKey": "尚未配置——在上方粘贴 API Key 并保存。",
135
+ "llm.src.credentials": "DSH 凭据",
136
+ "llm.src.env": "环境变量",
137
+ "llm.src.memory": "本机内存",
138
+ "llm.off": "未向 DSH 注册——勾选上方开关即可开启(id:{id})。",
139
+ "llm.registeredPending": "开关已开但注册尚未生效(id:{id})——等待下一次自动刷新;若持续未注册,检查 Host 日志。",
140
+ "llm.registered": "已注册 {id}:{models} 个模型,{vision} 个支持图片输入。",
141
+ "llm.noService": "registerProvider 已开启,但当前 Host 没有提供 LLM 注册服务。",
142
+ "llm.error": "提供方注册失败:{error}",
143
+ "llm.id": "提供方 ID:{id}(勾选开关后生效)",
144
+ "llm.switch": "向 DSH 注册(立即生效)",
145
+ "llm.switchBusy": "切换中…",
146
+ "llm.switchError": "切换失败:{error}",
147
+ "llm.roster": "推送到 DSH 的模型",
148
+ "llm.rosterHint": "勾选决定哪些模型推送进 DSH 模型列表;目录新增的模型默认不推送。",
149
+ "llm.rosterEmpty": "还没有可推送的模型——先在「API Key」卡片保存一次 Key 再回来。",
150
+ "llm.rosterSearchPlaceholder": "搜索模型名或 ID",
151
+ "llm.rosterCount": "已勾选 {selected} / 共 {total}",
152
+ "llm.rosterAll": "全部勾选",
153
+ "llm.rosterNone": "全部取消",
154
+ "llm.rosterSave": "保存",
155
+ "llm.rosterSaving": "保存中…",
156
+ "llm.rosterDiscard": "撤销",
157
+ "llm.rosterSaved": "已保存,模型列表已更新。",
158
+ "llm.rosterUnsaved": "有未保存的改动",
159
+ "llm.rosterError": "保存失败:{error}",
160
+ "llm.rosterNoMatch": "没有匹配的模型。",
161
+ "llm.rosterVision": "可看图",
162
+ "llm.rosterExhausted": "额度耗尽",
163
+ "llm.rosterRateTitle": "积分消耗伪倍率(自定义对比用,非官方)",
164
+ "llm.metaOutput": "最大输出 {out}",
165
+ "llm.metaLevels": "思考 {levels}",
166
+ "llm.rosterThinkingDefault": "思考强度默认 {level}",
167
+ "llm.level.off": "关闭",
168
+ "llm.level.minimal": "微量",
169
+ "llm.level.low": "低",
170
+ "llm.level.medium": "中",
171
+ "llm.level.high": "高",
172
+ "llm.level.xhigh": "极高",
173
+ "llm.level.max": "最高",
174
+ "draw.title": "出图工具",
175
+ "draw.switch": "注册出图工具 agnes_draw_image(下次 Host 装载生效)",
176
+ "draw.switchBusy": "切换中…",
177
+ "draw.switchError": "切换失败:{error}",
178
+ "draw.on": "agent 可用{model}生成图片。",
179
+ "draw.onList": "agent 出图将使用以下模型:",
180
+ "draw.badge": "出图",
181
+ "draw.badgeNone": "暂无可选模型",
182
+ "draw.badgeAuto": "{badge} · 自动选择",
183
+ "draw.badgePinned": "{badge} · 配置已指定",
184
+ "draw.candidates": "共 {count} 个出图模型",
185
+ "draw.autoOption": "自动选择(目录第一个出图模型)",
186
+ "draw.effective": "当前生效",
187
+ "draw.off": "未注册——勾选上方开关即可开启。",
188
+ "draw.noTools": "drawEnabled 已开启,但当前 Host 没有提供 agent tools 注册服务,工具静默缺席。",
189
+ "draw.needsKey": "尚未配置 API Key;保存后即可出图。",
190
+ "draw.noCandidates": "当前 API Key 目录里暂无出图模型;出图不可用。",
191
+ "draw.modelFallback": "第一个可用模型",
192
+ "video.title": "视频工具",
193
+ "video.switch": "注册视频工具 agnes_video_generate(下次 Host 装载生效)",
194
+ "video.switchBusy": "切换中…",
195
+ "video.switchError": "切换失败:{error}",
196
+ "video.onList": "agent 生成视频将使用以下模型:",
197
+ "video.badge": "视频",
198
+ "video.badgeNone": "暂无可选模型",
199
+ "video.autoOption": "自动选择(目录第一个 V2.0 视频模型;无 V2.0 时取第一个 2.5)",
200
+ "video.effective": "当前生效",
201
+ "video.off": "未注册——勾选上方开关即可开启。",
202
+ "video.noTools": "videoEnabled 已开启,但当前 Host 没有提供 agent tools 注册服务,工具静默缺席。",
203
+ "video.needsKey": "尚未配置 API Key;保存后即可生成视频。",
204
+ "video.noCandidates": "当前 API Key 目录里暂无视频模型;视频不可用。",
205
+ "video.note25": "其中 {count} 个为 2.5 系列视频模型({ids}):走秒数制参数(seconds / size / aspect_ratio),工具已支持并自动换算;选中时按 2.5 体系出片。",
206
+ "note": "数据来自 Agnes 控制台 API(/api/usage/overview、/api/usage/series、/api/cn/user/subscription 与公开的套餐目录),Host 侧缓存 {cache} 秒;令牌失效后 Host 会用已保存的账号自动重新登录一次。",
207
+ "tab.quota": "积分额度",
208
+ "tab.api": "接入 API",
209
+ "tab.raccoon": "小浣熊",
210
+ "raccoon.title": "小浣熊(商汤)",
211
+ "raccoon.desc": "接入 xiaohuanxiong.com 网关:微信扫码登录,模型经 DSH 提供方注册后可对话。与积分池相互独立。",
212
+ "raccoon.switch": "启用小浣熊提供方(向 DSH 注册模型)",
213
+ "raccoon.switchBusy": "切换中…",
214
+ "raccoon.switchError": "切换失败:{error}",
215
+ "raccoon.login": "微信扫码登录",
216
+ "raccoon.loggingIn": "等待扫码确认…",
217
+ "raccoon.logout": "退出登录",
218
+ "raccoon.loggedIn": "已登录:{nick}",
219
+ "raccoon.notLogged": "未登录——请先微信扫码。",
220
+ "raccoon.balance": "积分余额 {balance}",
221
+ "raccoon.models": "模型({count})",
222
+ "raccoon.free": "free",
223
+ "raccoon.rateTitle": "网关目录声明的积分倍率(0 为免费)",
224
+ "raccoon.registered": "已注册 raccoon 提供方:{count} 个模型。",
225
+ "raccoon.unregistered": "未注册——勾选上方开关即可开启。",
226
+ "raccoon.awaitingLogin": "已启用——登录后即可注册模型。",
227
+ "raccoon.error": "小浣熊操作失败:{error}",
228
+ "tab.agnescode": "AgnesCode",
229
+ "agnescode.title": "AgnesCode(爱思编程)",
230
+ "agnescode.desc": "读取本机 AgnesCode 桌面端的登录态(微信登录在桌面 App 内完成,本插件不碰你的密码),模型经 DSH 提供方注册后可对话。凭据独立,与积分额度 / 小浣熊互不影响。",
231
+ "agnescode.switch": "启用 AgnesCode 提供方(向 DSH 注册模型)",
232
+ "agnescode.switchError": "切换失败:{error}",
233
+ "agnescode.harvest": "检测本机登录态",
234
+ "agnescode.harvesting": "正在读取本机登录态…",
235
+ "agnescode.logout": "解除关联",
236
+ "agnescode.loggedIn": "已关联:{nick}",
237
+ "agnescode.bffBase": "接口地址 {base}",
238
+ "agnescode.notLogged": "未关联——请先在 AgnesCode 桌面端登录(微信扫码),再点「检测本机登录态」。",
239
+ "agnescode.balance": "积分余额 {balance}",
240
+ "agnescode.balanceDetail": "时效 {timeSensitive} · 永久 {permanent}",
241
+ "agnescode.models": "模型({count})",
242
+ "agnescode.memberOnly": "会员",
243
+ "agnescode.registered": "已注册 agnescode 提供方:{count} 个模型。",
244
+ "agnescode.unregistered": "未注册——勾选上方开关即可开启。",
245
+ "agnescode.awaitingHarvest": "已启用——检测到本机登录态后即可注册模型。",
246
+ "agnescode.expiresAt": "凭据有效期至 {time}",
247
+ "agnescode.error": "AgnesCode 操作失败:{error}",
248
+ "agnescode.harvestOk": "已读取本机登录态。",
249
+ "agnescode.harvestFail": "未能在本机找到可用的 AgnesCode 登录态。探测记录:",
250
+ "agnescode.tier.file_missing": "文件不存在",
251
+ "agnescode.tier.unreadable": "读不了(权限?)",
252
+ "agnescode.tier.malformed": "内容不是预期格式",
253
+ "agnescode.tier.no_key": "加密密钥拿不到(Local State 缺失或形状已变)",
254
+ "agnescode.tier.decrypt_failed": "解密失败(App 的加密方式可能已变更)",
255
+ "agnescode.tier.no_token": "会话里没有令牌(App 未登录?)",
256
+ "agnescode.tier.untrusted_base": "会话的接口地址不在 Agnes 域内(已拒绝)",
257
+ "agnescode.tier.unsupported_platform": "当前平台没有已知的桌面 App 数据目录",
258
+ "agnescode.tier.ok": "成功"
259
+ };
260
+ en = {
261
+ "entry.label": "Credits",
262
+ "panel.title": "Credits",
263
+ "panel.back": "Back to conversation",
264
+ "panel.refresh": "Refresh",
265
+ "panel.updated": "Updated {time}",
266
+ "panel.loading": "Loading…",
267
+ "panel.error": "Could not read: {error}",
268
+ "panel.jwtMissing": "No Agnes console account is configured yet.",
269
+ "panel.jwtExpired": "The console token is no longer valid. Agnes issues no refresh token, so the Host signs in again with the saved account; if that also fails, sign in manually.",
270
+ "panel.configError": "The plugin is misconfigured: {error}",
271
+ "panel.consoleTransient": "The Agnes console could not be read just now. This usually clears on the next automatic refresh; if it persists, check your network and try again shortly.",
272
+ "panel.shapeDrift": "The upstream payload shape may have changed: {detail}",
273
+ "auth.title": "Connect the Agnes console",
274
+ "auth.registerHint": "No account yet? Register on the official site for a free Token Plan quota →",
275
+ "auth.portalHint": "Manage quota / get API keys on the official site →",
276
+ "auth.username": "Email",
277
+ "auth.password": "Password",
278
+ "auth.show": "Show",
279
+ "auth.hide": "Hide",
280
+ "auth.placeholderUser": "The email you registered on platform.agnes-ai.cn",
281
+ "auth.submit": "Sign in",
282
+ "auth.submitting": "Signing in…",
283
+ "auth.working": "Saved and signed in; reading quota…",
284
+ "auth.forget": "Forget the saved account",
285
+ "auth.forgotten": "Account cleared (the current token still works)",
286
+ "auth.saved": "Email and login token stored in the DSH credentials; the password is never written to disk. Agnes issues no refresh token, so a dead token means the Host signs in again with the saved email and password.",
287
+ "auth.ephemeral": "Note: this Host has no credentials service, so the account lives in memory only and must be entered again after a restart.",
288
+ "auth.autoRecoverOn": "Auto-recover: on (a dead token re-signs in with the saved email and password)",
289
+ "auth.autoRecoverOff": "Auto-recover: off (a dead token means signing in again manually)",
290
+ "auth.badCredentials": "That email or password is not right",
291
+ "auth.locked": "This account is locked. Agnes locks an account after repeated failed sign-ins — check the account in the Agnes console before retrying; the panel will not retry on its own in the meantime.",
292
+ "auth.retryAfter": "The platform asks to wait about {minutes} more minutes. The panel will not retry on its own during that window, so the lock is not extended.",
293
+ "auth.rateLimited": "Too many attempts. Wait a moment and try again.",
294
+ "auth.verification": "This sign-in needs an extra step (SMS or captcha) that automation cannot complete. Sign in once in a browser first.",
295
+ "auth.failed": "Sign-in did not complete: {reason}",
296
+ "auth.empty": "Enter a username and a password",
297
+ "auth.network": "Could not reach the local Host",
298
+ "panel.empty": "No data yet.",
299
+ "section.quota": "Quota limits",
300
+ "section.usage": "Usage (last {days} days)",
301
+ "quota.window": "Quota",
302
+ "quota.win.requests5h": "Model requests",
303
+ "quota.win.requestsWeekly": "Weekly requests",
304
+ "quota.win.imagesDaily": "Images",
305
+ "quota.win.videoDaily": "Video",
306
+ "quota.perHours": "{hours} hours",
307
+ "quota.perDay": "per day",
308
+ "quota.perWeek": "per week",
309
+ "quota.perMonth": " / mo",
310
+ "quota.perYear": " / yr",
311
+ "quota.cycle.monthly": "monthly",
312
+ "quota.cycle.yearly": "yearly",
313
+ "quota.unit.requests": "",
314
+ "quota.unit.images": "",
315
+ "quota.used": "Used",
316
+ "quota.expires": "expires {time}",
317
+ "quota.none": "No quota data yet.",
318
+ "quota.planUnknown": "Could not identify the current plan — the console returned nothing matchable. The plan comparison below is the platform's public catalogue and needs no sign-in.",
319
+ "quota.consoleOffline": "The Agnes console is not connected, so the quota and usage on this page could not be read.",
320
+ "quota.consoleOfflineHint": "The sign-in form is in the \"{section}\" card further down this page.",
321
+ "quota.error": "Could not read {source}: {message}",
322
+ "quota.catalogue": "Compare plans ({count})",
323
+ "quota.windowNote": "Agnes rate-limits by window: the limits above are counts per N hours / day / week. The console publishes cumulative account usage only, never a per-window figure — so the totals below cover a different period and must not be subtracted from the limits above.",
324
+ "quota.accountTotals": "Account totals (as the console reports them)",
325
+ "quota.total.requests": "Model requests",
326
+ "quota.total.tokens": "Text tokens",
327
+ "quota.total.images": "Images",
328
+ "quota.total.video": "Video seconds",
329
+ "quota.total.activeDays": "Active days",
330
+ "quota.usageMissing": "{label}: not read yet.",
331
+ "usage.none": "No usage recorded in this range.",
332
+ "usage.requests": "Model requests",
333
+ "usage.perBucket": "as bucketed by the platform",
334
+ "usage.legend": "Bars are scaled relative to the busiest bucket, not to the quota limit.",
335
+ "shape.api": "endpoint",
336
+ "shape.missing": "missing field",
337
+ "section.collapse": "Collapse",
338
+ "section.expand": "Expand",
339
+ "quota.vision": "Vision-capable: {models}",
340
+ "quota.visionInferred": "(inferred from model names; not declared by the platform)",
341
+ "llm.contextBadge": "{ctx} context",
342
+ "auth.selfRenew": "Token renews itself",
343
+ "auth.needsLogin": "Sign-in required",
344
+ "llm.title": "API key",
345
+ "llm.providerTitle": "Language models",
346
+ "llm.keyField": "API key",
347
+ "llm.keyEditor": "Change / clear the API key",
348
+ "llm.placeholder": "Paste your API key (free sk- or Token Plan cpk-)",
349
+ "llm.save": "Save API key",
350
+ "llm.saving": "Saving…",
351
+ "llm.forget": "Forget the saved API key",
352
+ "llm.saved": "API key stored in the DSH credentials; the next poll fetches the model catalog.",
353
+ "llm.forgotten": "Panel-saved API key cleared (an AGNES_TOKEN_PLAN_API_KEY environment value is left untouched).",
354
+ "llm.empty": "Enter an API key",
355
+ "llm.footnote": "The key is kept only in the DSH credentials, never in this plugin's folder or logs; it is read per request.",
356
+ "llm.keyRegisterHint": "No API key yet? Get a free one on the official site →",
357
+ "llm.ephemeral": "Note: this Host has no credentials service, so the key lives in memory only and is lost on restart.",
358
+ "llm.keyPresent": "Configured (source: {source})",
359
+ "llm.noKey": "Not configured — paste an API key above and save.",
360
+ "llm.src.credentials": "DSH credentials",
361
+ "llm.src.env": "environment",
362
+ "llm.src.memory": "memory",
363
+ "llm.off": "Not registered with DSH — tick the switch above (id: {id}).",
364
+ "llm.registeredPending": "Switch is on but registration has not landed yet (id: {id}) — wait for the next automatic refresh; if it persists, check the Host logs.",
365
+ "llm.registered": "Registered {id}: {models} model(s), {vision} with image input.",
366
+ "llm.noService": "registerProvider is on, but this Host exposes no LLM registration service.",
367
+ "llm.error": "Provider registration failed: {error}",
368
+ "llm.id": "Provider id: {id} (takes effect once the switch is ticked)",
369
+ "llm.switch": "Register with DSH (immediate)",
370
+ "llm.switchBusy": "Switching…",
371
+ "llm.switchError": "Switch failed: {error}",
372
+ "llm.roster": "Models pushed to DSH",
373
+ "llm.rosterHint": "Ticking decides which models are pushed into DSH's model list; models the catalogue gains later are not pushed by default.",
374
+ "llm.rosterEmpty": "No models to push yet - save a key in the API key card first.",
375
+ "llm.rosterSearchPlaceholder": "Search a model name or id",
376
+ "llm.rosterCount": "{selected} ticked / {total} total",
377
+ "llm.rosterAll": "Tick all",
378
+ "llm.rosterNone": "Untick all",
379
+ "llm.rosterSave": "Save",
380
+ "llm.rosterSaving": "Saving…",
381
+ "llm.rosterDiscard": "Discard",
382
+ "llm.rosterSaved": "Saved - the model list has been updated.",
383
+ "llm.rosterUnsaved": "Unsaved changes",
384
+ "llm.rosterError": "Save failed: {error}",
385
+ "llm.rosterNoMatch": "No model matches.",
386
+ "llm.rosterVision": "vision",
387
+ "llm.rosterExhausted": "quota exhausted",
388
+ "llm.rosterRateTitle": "Pseudo credit multiplier (custom comparison aid, not official)",
389
+ "llm.metaOutput": "max output {out}",
390
+ "llm.metaLevels": "thinking {levels}",
391
+ "llm.rosterThinkingDefault": "thinking default {level}",
392
+ "llm.level.off": "off",
393
+ "llm.level.minimal": "minimal",
394
+ "llm.level.low": "low",
395
+ "llm.level.medium": "medium",
396
+ "llm.level.high": "high",
397
+ "llm.level.xhigh": "xhigh",
398
+ "llm.level.max": "max",
399
+ "draw.title": "Draw tool",
400
+ "draw.switch": "Register the agnes_draw_image tool (next Host mount)",
401
+ "draw.switchBusy": "Switching…",
402
+ "draw.switchError": "Switch failed: {error}",
403
+ "draw.on": "The agent can generate images with {model}.",
404
+ "draw.onList": "Draw calls will use the model below:",
405
+ "draw.badge": "image",
406
+ "draw.badgeNone": "no model available yet",
407
+ "draw.badgeAuto": "{badge} · auto-picked",
408
+ "draw.badgePinned": "{badge} · pinned in config",
409
+ "draw.candidates": "{count} image-capable model(s)",
410
+ "draw.autoOption": "Auto — first image-capable model in the catalogue",
411
+ "draw.effective": "active",
412
+ "draw.off": "Not registered — tick the switch above.",
413
+ "draw.noTools": "drawEnabled is on, but this Host exposes no agent tools service; the tool is silently absent.",
414
+ "draw.needsKey": "No API key yet; save one to start generating images.",
415
+ "draw.noCandidates": "This API key's catalogue has no image model; drawing is unavailable.",
416
+ "draw.modelFallback": "the first available model",
417
+ "video.title": "Video tool",
418
+ "video.switch": "Register the agnes_video_generate tool (next Host mount)",
419
+ "video.switchBusy": "Switching…",
420
+ "video.switchError": "Switch failed: {error}",
421
+ "video.onList": "Video calls will use the model below:",
422
+ "video.badge": "video",
423
+ "video.badgeNone": "no model available yet",
424
+ "video.autoOption": "Auto — first V2.0 video model in the catalogue (first 2.5 when the catalogue has no V2.0 one)",
425
+ "video.effective": "active",
426
+ "video.off": "Not registered — tick the switch above.",
427
+ "video.noTools": "videoEnabled is on, but this Host exposes no agent tools service; the tool is silently absent.",
428
+ "video.needsKey": "No API key yet; save one to start generating video.",
429
+ "video.noCandidates": "This API key's catalogue has no video model; video is unavailable.",
430
+ "video.note25": "{count} of these are 2.5-series video model(s) ({ids}): they take the whole-second scheme (seconds / size / aspect_ratio); the tool supports them and adapts automatically when one is selected.",
431
+ "note": "Data from the Agnes console API (/api/usage/overview, /api/usage/series, /api/cn/user/subscription, and the public plan catalogue), cached {cache}s on the Host; a dead token makes the Host sign in again with the saved account.",
432
+ "tab.quota": "Quota & Usage",
433
+ "tab.api": "API Integration",
434
+ "tab.raccoon": "Raccoon",
435
+ "raccoon.title": "Raccoon (SenseNova)",
436
+ "raccoon.desc": "Connects the xiaohuanxiong.com gateway: WeChat QR sign-in, models registered with DSH. Independent of the credit pools.",
437
+ "raccoon.switch": "Register Raccoon with DSH",
438
+ "raccoon.switchBusy": "Switching…",
439
+ "raccoon.switchError": "Switch failed: {error}",
440
+ "raccoon.login": "Sign in with WeChat QR",
441
+ "raccoon.loggingIn": "Waiting for QR confirmation…",
442
+ "raccoon.logout": "Sign out",
443
+ "raccoon.loggedIn": "Signed in: {nick}",
444
+ "raccoon.notLogged": "Not signed in — scan the QR code first.",
445
+ "raccoon.balance": "Balance {balance}",
446
+ "raccoon.models": "Models ({count})",
447
+ "raccoon.free": "free",
448
+ "raccoon.rateTitle": "Credit multiplier as declared by the gateway catalogue (0 = free)",
449
+ "raccoon.registered": "Raccoon registered with DSH: {count} model(s).",
450
+ "raccoon.unregistered": "Not registered — tick the switch above.",
451
+ "raccoon.awaitingLogin": "Enabled — log in to register the models.",
452
+ "raccoon.error": "Raccoon operation failed: {error}",
453
+ "tab.agnescode": "AgnesCode",
454
+ "agnescode.title": "AgnesCode",
455
+ "agnescode.desc": "Uses the locally signed-in AgnesCode desktop App (the WeChat login happens inside the App — this plugin never touches your password). Models register with DSH as a provider. Credentials stay independent of the quota and Raccoon tabs.",
456
+ "agnescode.switch": "Enable the AgnesCode provider (register models with DSH)",
457
+ "agnescode.switchError": "Switch failed: {error}",
458
+ "agnescode.harvest": "Detect local login state",
459
+ "agnescode.harvesting": "Reading the local login state…",
460
+ "agnescode.logout": "Unlink",
461
+ "agnescode.loggedIn": "Linked: {nick}",
462
+ "agnescode.bffBase": "API base {base}",
463
+ "agnescode.notLogged": "Not linked — sign in inside the AgnesCode desktop App (WeChat scan) first, then run the detection.",
464
+ "agnescode.balance": "Credit balance {balance}",
465
+ "agnescode.balanceDetail": "Time-limited {timeSensitive} · permanent {permanent}",
466
+ "agnescode.models": "Models ({count})",
467
+ "agnescode.memberOnly": "member",
468
+ "agnescode.registered": "AgnesCode registered with DSH: {count} model(s).",
469
+ "agnescode.unregistered": "Not registered — tick the switch above.",
470
+ "agnescode.awaitingHarvest": "Enabled — runs once a local login state is detected.",
471
+ "agnescode.expiresAt": "Credential valid until {time}",
472
+ "agnescode.error": "AgnesCode operation failed: {error}",
473
+ "agnescode.harvestOk": "Local login state read.",
474
+ "agnescode.harvestFail": "No usable AgnesCode login state found on this machine. Probed:",
475
+ "agnescode.tier.file_missing": "file missing",
476
+ "agnescode.tier.unreadable": "unreadable (permissions?)",
477
+ "agnescode.tier.malformed": "unexpected content shape",
478
+ "agnescode.tier.no_key": "encryption key unavailable (Local State missing or reshaped)",
479
+ "agnescode.tier.decrypt_failed": "decryption failed (the App's crypto may have changed)",
480
+ "agnescode.tier.no_token": "session carries no token (App not signed in?)",
481
+ "agnescode.tier.untrusted_base": "session's API base is outside the Agnes domains (refused)",
482
+ "agnescode.tier.unsupported_platform": "no known desktop App data directory for this platform",
483
+ "agnescode.tier.ok": "ok"
484
+ };
485
+ }));
486
+
487
+ //#endregion
488
+ //#region src/client/format.ts
489
+ /** Time and number formatters, verbatim from the pre-split `client.js`. */
490
+ /** `HH:MM` for one epoch second. */
491
+ function clock(epoch) {
492
+ if (typeof epoch !== "number" || !Number.isFinite(epoch) || epoch <= 0) return "—";
493
+ const date = /* @__PURE__ */ new Date(epoch * 1e3);
494
+ const pad = (value) => String(value).padStart(2, "0");
495
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
496
+ }
497
+ /** `MM-DD HH:mm` for one epoch second. */
498
+ function clockLong(epoch) {
499
+ if (typeof epoch !== "number" || !Number.isFinite(epoch) || epoch <= 0) return "—";
500
+ const date = /* @__PURE__ */ new Date(epoch * 1e3);
501
+ const pad = (value) => String(value).padStart(2, "0");
502
+ return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
503
+ }
504
+ /**
505
+ * A credit figure as text: 2-decimal precision under 10 000, whole with
506
+ * thousands separators at or above it. The switch is deliberate — a pool
507
+ * limit of 60 000 reads as "60,000", a live balance of 47.5 as "47.5".
508
+ */
509
+ function count(value) {
510
+ const number = typeof value === "number" && Number.isFinite(value) ? value : 0;
511
+ if (number >= 1e4) return Math.round(number).toLocaleString();
512
+ return String(Math.round(number * 100) / 100);
513
+ }
514
+ /** Fill a `{token}` template from a dictionary entry. */
515
+ function format(template, vars) {
516
+ let text = template;
517
+ for (const [key, value] of Object.entries(vars || {})) text = text.split(`{${key}}`).join(String(value));
518
+ return text;
519
+ }
520
+ /**
521
+ * A price as text, from the platform's minor units: `2500` + `cny` -> `¥25.00`.
522
+ *
523
+ * The catalogue quotes prices in cents (`price_minor`), so dividing by 100 is
524
+ * the platform's own convention rather than a guess. An unrecognised currency
525
+ * keeps its code rather than being dressed in the wrong symbol.
526
+ */
527
+ function money(minor, currency) {
528
+ const amount = typeof minor === "number" && Number.isFinite(minor) ? minor / 100 : 0;
529
+ const code = typeof currency === "string" ? currency.toLowerCase() : "";
530
+ const symbol = code === "cny" || code === "rmb" ? "¥" : code === "usd" ? "$" : "";
531
+ return symbol === "" ? `${amount.toFixed(2)} ${code}`.trim() : `${symbol}${amount.toFixed(2)}`;
532
+ }
533
+ /**
534
+ * A token count the way the platform names it: 1048576 → "1M", 65536 → "64K",
535
+ * 128000 → "128K". Returns "" for a figure that is not a positive number, so
536
+ * an unknown value draws no segment instead of a zero.
537
+ *
538
+ * Why two bases: the catalogue mixes them — windows and ceilings arrive as
539
+ * powers of two (1048576), while the plugin's own 128k fallback is the round
540
+ * decimal 128 000. A flat /1000 rounding once printed "1049k" for the 1M
541
+ * window and it read like a placeholder bug; so figures divisible by 1000 keep
542
+ * the decimal reading they were written with, binary-only figures (262144 →
543
+ * 256K, 65536 → 64K) get the binary one, and anything ≥ 1M goes to M.
544
+ */
545
+ function tokenSize(value) {
546
+ const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : 0;
547
+ if (number <= 0) return "";
548
+ if (number >= 1e6) return `${Math.round(number / 1e5) / 10}M`;
549
+ if (number % 1e3 === 0) return `${number / 1e3}K`;
550
+ if (number % 1024 === 0) return `${number / 1024}K`;
551
+ return `${Math.round(number / 1e3)}K`;
552
+ }
553
+ var init_format = __esmMin((() => {}));
554
+
555
+ //#endregion
556
+ //#region src/client/http.ts
557
+ /** One POST with the panel's fixed request shape; the parsed body or null. */
558
+ async function postRaw(path, payload) {
559
+ const response = await fetch(path, {
560
+ method: "POST",
561
+ headers: {
562
+ "content-type": "application/json",
563
+ accept: "application/json"
564
+ },
565
+ cache: "no-store",
566
+ body: JSON.stringify(payload)
567
+ });
568
+ const body = await response.json().catch(() => null);
569
+ return {
570
+ status: response.status,
571
+ body
572
+ };
573
+ }
574
+ /** POST and return the parsed body, or null when the response is not JSON. */
575
+ function postJson(path, payload) {
576
+ return postRaw(path, payload).then(({ body }) => body);
577
+ }
578
+ /** POST and demand `ok:true`; refuse by throwing the Host's own wording. */
579
+ async function postJsonOrThrow(path, payload) {
580
+ const { status, body } = await postRaw(path, payload);
581
+ if (body === null || body.ok !== true) throw new Error(typeof body?.error === "string" ? body.error : `HTTP ${status}`);
582
+ return body;
583
+ }
584
+ var init_http = __esmMin((() => {}));
585
+
586
+ //#endregion
587
+ //#region src/client/runtime.ts
588
+ /** Hand the loader-provided React to the rest of the client. One-shot. */
589
+ function provideClientReact(value) {
590
+ if (typeof value !== "object" || value === null) throw new Error("client: the loader did not hand over a react module");
591
+ api = value;
592
+ }
593
+ function reactApi() {
594
+ if (api === null) throw new Error("client: react used before clientFactory ran");
595
+ return api;
596
+ }
597
+ var api, h, useState, useEffect, useCallback, useMemo, useRef;
598
+ var init_runtime = __esmMin((() => {
599
+ api = null;
600
+ h = (type, props, ...children) => reactApi().createElement(type, props, ...children);
601
+ useState = (initial) => reactApi().useState(initial);
602
+ useEffect = (effect, deps) => reactApi().useEffect(effect, deps);
603
+ useCallback = (callback, deps) => reactApi().useCallback(callback, deps);
604
+ useMemo = (factory, deps) => reactApi().useMemo(factory, deps);
605
+ useRef = (initial) => reactApi().useRef(initial);
606
+ }));
607
+
608
+ //#endregion
609
+ //#region src/client/snapshot.ts
610
+ /**
611
+ * Read one snapshot response into the (data, error) pair the panel renders.
612
+ *
613
+ * The Host answers HTTP 200 for every expected outcome and signals the
614
+ * difference in the body: `ok:true` carries the numbers, `ok:false` carries
615
+ * a code and — crucially — the `auth` block, so a panel that cannot reach
616
+ * the console can still say whether its token will renew by itself.
617
+ *
618
+ * A named function at module scope, not inline branching, so the
619
+ * Node-side tests can drive the panel's REAL reading of a response by
620
+ * loading this bundle as a module (`client-surface.js`) — instead of a
621
+ * hand-written copy that would drift the moment either side is edited.
622
+ */
623
+ function interpretSnapshot(body) {
624
+ const payload = body;
625
+ if (payload && payload.ok === false) return {
626
+ data: null,
627
+ error: {
628
+ message: payload.error || "unexpected payload",
629
+ code: payload.code,
630
+ auth: payload.auth ?? null
631
+ }
632
+ };
633
+ if (!payload || payload.ok !== true) return {
634
+ data: null,
635
+ error: "unexpected payload"
636
+ };
637
+ return {
638
+ data: payload,
639
+ error: null
640
+ };
641
+ }
642
+ /**
643
+ * The failure a non-2xx snapshot response becomes.
644
+ *
645
+ * A non-2xx carries no body, so the status is the only clue. 401/403 mean
646
+ * the token is gone — the same story as the Host's own `jwt_expired`, and
647
+ * the only reading that keeps the sign-in form on screen instead of leaving
648
+ * the reader with a bare status code. Anything else is a plain transport
649
+ * string, which keeps the form reachable too.
650
+ *
651
+ * Named and module-scoped for the same reason as `interpretSnapshot`: the
652
+ * Node-side tests drive this mapping instead of a copy of it.
653
+ */
654
+ function errorOfStatus(status) {
655
+ if (status === 401 || status === 403) return {
656
+ message: `HTTP ${status}`,
657
+ code: "jwt_expired",
658
+ auth: null
659
+ };
660
+ return `HTTP ${status}`;
661
+ }
662
+ /**
663
+ * The panel's decision: what this snapshot means for what to show.
664
+ *
665
+ * Deliberately a module-scope pure function in `(data, error, tt)`.
666
+ * `PanelPage` calls it in the browser, and the Node-side tests call the
667
+ * very same function after loading this bundle as a module (see
668
+ * `client-surface.js`): no source text is copied or scraped, so the
669
+ * tested logic and the running logic cannot drift apart.
670
+ */
671
+ function viewOf(data, error, tt) {
672
+ const failure = error === null || error === void 0 ? null : typeof error === "string" ? {
673
+ message: error,
674
+ code: null,
675
+ auth: null
676
+ } : error;
677
+ const auth = data?.auth ?? failure?.auth ?? null;
678
+ const code = failure?.code ?? data?.quota?.error?.code ?? null;
679
+ const needsSetup = (data === null || data.quota?.consoleConnected === false) && !FORM_EXCLUDED_CODES.has(code);
680
+ const guidanceKey = code === null ? null : GUIDANCE_BY_CODE[code] ?? null;
681
+ return {
682
+ failure,
683
+ auth,
684
+ needsSetup,
685
+ guidanceKey,
686
+ guidance: guidanceKey === null ? null : guidanceKey === "panel.configError" ? format(tt(guidanceKey), { error: failure?.message ?? data?.quota?.error?.message }) : tt(guidanceKey),
687
+ shapeWarnings: Array.isArray(data?.shapeWarnings) ? data.shapeWarnings : []
688
+ };
689
+ }
690
+ var GUIDANCE_BY_CODE, FORM_EXCLUDED_CODES, REFUSAL_TEXT;
691
+ var init_snapshot = __esmMin((() => {
692
+ init_format();
693
+ GUIDANCE_BY_CODE = Object.freeze({
694
+ auth_error: "panel.jwtExpired",
695
+ jwt_expired: "panel.jwtExpired",
696
+ no_refresh_token: "panel.jwtExpired",
697
+ refresh_rejected: "panel.jwtExpired",
698
+ jwks: "panel.jwtExpired",
699
+ login_flow: "panel.jwtExpired",
700
+ token_rejected: "panel.jwtExpired",
701
+ refresh_failed: "panel.jwtExpired",
702
+ not_configured: "panel.jwtMissing",
703
+ missing_credentials: "panel.jwtMissing",
704
+ login_rejected: "auth.badCredentials",
705
+ login_failed: "auth.badCredentials",
706
+ account_locked: "auth.locked",
707
+ rate_limited: "auth.rateLimited",
708
+ verification_required: "auth.verification",
709
+ config_error: "panel.configError",
710
+ console_error: "panel.consoleTransient"
711
+ });
712
+ FORM_EXCLUDED_CODES = Object.freeze(/* @__PURE__ */ new Set(["config_error", "console_error"]));
713
+ REFUSAL_TEXT = Object.freeze({
714
+ login_rejected: "auth.badCredentials",
715
+ account_locked: "auth.locked",
716
+ rate_limited: "auth.rateLimited",
717
+ verification_required: "auth.verification"
718
+ });
719
+ }));
720
+
721
+ //#endregion
722
+ //#region src/client/styles.ts
723
+ var BUTTON, S;
724
+ var init_styles = __esmMin((() => {
725
+ BUTTON = {
726
+ height: 30,
727
+ padding: "0 12px",
728
+ borderRadius: 8,
729
+ border: "1px solid var(--dsw-alias-border-l2)",
730
+ background: "var(--dsw-alias-bg-layer-2)",
731
+ color: "var(--dsw-alias-label-primary)",
732
+ fontSize: 13,
733
+ cursor: "pointer"
734
+ };
735
+ S = {
736
+ page: {
737
+ flex: "1 1 auto",
738
+ height: "100%",
739
+ minHeight: 0,
740
+ display: "flex",
741
+ flexDirection: "column",
742
+ overflow: "hidden",
743
+ color: "var(--dsw-alias-label-primary)",
744
+ fontSize: 14,
745
+ lineHeight: "22px"
746
+ },
747
+ headerBar: {
748
+ flex: "none",
749
+ background: "var(--dsw-alias-bg-base)",
750
+ position: "relative",
751
+ zIndex: 1
752
+ },
753
+ header: {
754
+ display: "flex",
755
+ alignItems: "center",
756
+ gap: 12,
757
+ padding: "16px 0 12px"
758
+ },
759
+ scroll: {
760
+ flex: 1,
761
+ minHeight: 0,
762
+ overflowY: "auto",
763
+ overflowX: "hidden"
764
+ },
765
+ content: { padding: "6px 0 56px" },
766
+ tabBar: {
767
+ display: "flex",
768
+ gap: 4,
769
+ borderBottom: "1px solid var(--dsw-alias-border-l1)",
770
+ marginBottom: 4
771
+ },
772
+ tab: {
773
+ appearance: "none",
774
+ background: "none",
775
+ border: "none",
776
+ borderBottom: "2px solid transparent",
777
+ padding: "8px 12px",
778
+ fontSize: 13,
779
+ color: "var(--dsw-alias-label-secondary)",
780
+ cursor: "pointer"
781
+ },
782
+ tabActive: {
783
+ color: "var(--dsw-alias-label-primary)",
784
+ fontWeight: 600,
785
+ borderBottom: "2px solid var(--agnes-brand, #6C5CE7)"
786
+ },
787
+ title: {
788
+ margin: 0,
789
+ fontSize: 20,
790
+ fontWeight: 600,
791
+ lineHeight: "28px"
792
+ },
793
+ updated: {
794
+ color: "var(--dsw-alias-label-secondary)",
795
+ fontSize: 12
796
+ },
797
+ spacer: { flex: 1 },
798
+ button: BUTTON,
799
+ sectionTitle: {
800
+ margin: "22px 0 10px",
801
+ fontSize: 13,
802
+ fontWeight: 600,
803
+ color: "var(--dsw-alias-label-secondary)"
804
+ },
805
+ sectionCard: {
806
+ border: "1px solid var(--dsw-alias-border-l1)",
807
+ borderRadius: 12,
808
+ background: "var(--dsw-alias-bg-layer-1)",
809
+ overflow: "hidden",
810
+ marginTop: 22
811
+ },
812
+ sectionHead: {
813
+ display: "flex",
814
+ alignItems: "center",
815
+ gap: 12,
816
+ width: "100%",
817
+ padding: "12px 16px",
818
+ background: "none",
819
+ border: "none",
820
+ cursor: "pointer",
821
+ textAlign: "left"
822
+ },
823
+ sectionHeadTitle: {
824
+ flex: 1,
825
+ minWidth: 0,
826
+ fontSize: 15,
827
+ fontWeight: 600,
828
+ color: "var(--dsw-alias-label-primary)"
829
+ },
830
+ chevron: {
831
+ display: "inline-flex",
832
+ flex: "none",
833
+ transition: "transform 0.15s ease",
834
+ color: "var(--dsw-alias-label-secondary)"
835
+ },
836
+ chevronOpen: { transform: "rotate(180deg)" },
837
+ sectionBody: {
838
+ borderTop: "1px solid var(--dsw-alias-border-l1)",
839
+ margin: "0 16px",
840
+ padding: "12px 0 16px"
841
+ },
842
+ card: {
843
+ background: "var(--dsw-alias-bg-layer-1)",
844
+ border: "1px solid var(--dsw-alias-border-l1)",
845
+ borderRadius: 12,
846
+ padding: 16
847
+ },
848
+ poolsGrid: {
849
+ display: "grid",
850
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 320px), 1fr))",
851
+ gap: 12,
852
+ alignItems: "start"
853
+ },
854
+ cardHead: {
855
+ display: "flex",
856
+ alignItems: "center",
857
+ gap: 10,
858
+ flexWrap: "wrap"
859
+ },
860
+ poolName: {
861
+ fontSize: 15,
862
+ fontWeight: 600
863
+ },
864
+ chip: {
865
+ display: "inline-flex",
866
+ alignItems: "center",
867
+ height: 22,
868
+ padding: "0 8px",
869
+ borderRadius: 999,
870
+ fontSize: 12,
871
+ border: "1px solid var(--dsw-alias-border-l1)",
872
+ background: "var(--dsw-alias-bg-layer-2)",
873
+ color: "var(--dsw-alias-label-secondary)"
874
+ },
875
+ grantChip: {
876
+ display: "inline-flex",
877
+ alignItems: "center",
878
+ fontSize: 13,
879
+ color: "var(--dsw-alias-label-primary)",
880
+ fontVariantNumeric: "tabular-nums"
881
+ },
882
+ quotas: {
883
+ display: "grid",
884
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 170px), 1fr))",
885
+ gap: 10,
886
+ marginTop: 14
887
+ },
888
+ quota: {
889
+ display: "flex",
890
+ flexDirection: "column",
891
+ gap: 8,
892
+ minWidth: 0,
893
+ padding: "12px 14px",
894
+ borderRadius: 10,
895
+ background: "var(--dsw-alias-bg-layer-2)"
896
+ },
897
+ quotaTop: {
898
+ display: "flex",
899
+ alignItems: "center",
900
+ justifyContent: "space-between",
901
+ gap: 8,
902
+ flexWrap: "wrap"
903
+ },
904
+ quotaLabel: {
905
+ fontSize: 12,
906
+ fontWeight: 500,
907
+ color: "var(--dsw-alias-label-secondary)"
908
+ },
909
+ quotaReset: {
910
+ fontSize: 11,
911
+ color: "var(--dsw-alias-label-secondary)"
912
+ },
913
+ quotaRemaining: {
914
+ fontSize: 18,
915
+ lineHeight: "22px",
916
+ fontWeight: 650,
917
+ letterSpacing: "-0.02em",
918
+ fontVariantNumeric: "tabular-nums"
919
+ },
920
+ quotaUsed: {
921
+ fontSize: 11,
922
+ lineHeight: "15px",
923
+ color: "var(--dsw-alias-label-secondary)",
924
+ fontVariantNumeric: "tabular-nums"
925
+ },
926
+ bar: {
927
+ height: 6,
928
+ borderRadius: 3,
929
+ background: "var(--dsw-alias-bg-layer-1)",
930
+ overflow: "hidden"
931
+ },
932
+ barFill: {
933
+ height: "100%",
934
+ borderRadius: 3,
935
+ background: "var(--agnes-brand, #6C5CE7)"
936
+ },
937
+ barFillWarn: { background: "var(--dsw-alias-state-warn-primary)" },
938
+ barFillError: { background: "var(--dsw-alias-state-error-primary)" },
939
+ details: {
940
+ marginTop: 12,
941
+ paddingTop: 10,
942
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
943
+ },
944
+ detailsSummary: {
945
+ fontSize: 12,
946
+ color: "var(--dsw-alias-label-secondary)",
947
+ cursor: "pointer",
948
+ userSelect: "none"
949
+ },
950
+ detailsBody: {
951
+ display: "flex",
952
+ flexDirection: "column",
953
+ gap: 10,
954
+ marginTop: 10
955
+ },
956
+ grant: {
957
+ fontSize: 12,
958
+ color: "var(--dsw-alias-label-secondary)"
959
+ },
960
+ models: {
961
+ display: "flex",
962
+ flexWrap: "wrap",
963
+ gap: 6
964
+ },
965
+ modelTag: {
966
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
967
+ fontSize: 11,
968
+ padding: "2px 6px",
969
+ borderRadius: 6,
970
+ background: "var(--dsw-alias-bg-layer-2)",
971
+ border: "1px solid var(--dsw-alias-border-l1)"
972
+ },
973
+ trendHead: {
974
+ display: "flex",
975
+ alignItems: "baseline",
976
+ justifyContent: "space-between",
977
+ gap: 12,
978
+ paddingBottom: 6,
979
+ borderBottom: "1px solid var(--dsw-alias-border-l1)"
980
+ },
981
+ trendHeadLabel: {
982
+ fontSize: 12,
983
+ color: "var(--dsw-alias-label-secondary)",
984
+ fontWeight: 500
985
+ },
986
+ trendRow: {
987
+ display: "flex",
988
+ flexDirection: "column",
989
+ gap: 8,
990
+ padding: "10px 0",
991
+ borderBottom: "1px solid var(--dsw-alias-border-l1)"
992
+ },
993
+ trendRowHead: {
994
+ display: "flex",
995
+ alignItems: "baseline",
996
+ justifyContent: "space-between",
997
+ gap: 12,
998
+ minWidth: 0
999
+ },
1000
+ trendModel: {
1001
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1002
+ fontSize: 12,
1003
+ minWidth: 0,
1004
+ overflow: "hidden",
1005
+ textOverflow: "ellipsis",
1006
+ whiteSpace: "nowrap"
1007
+ },
1008
+ trendCredits: {
1009
+ fontSize: 13,
1010
+ fontWeight: 600,
1011
+ fontVariantNumeric: "tabular-nums"
1012
+ },
1013
+ trendBar: {
1014
+ height: 6,
1015
+ borderRadius: 3,
1016
+ background: "var(--dsw-alias-bg-layer-2)",
1017
+ overflow: "hidden"
1018
+ },
1019
+ trendLegend: {
1020
+ marginTop: 10,
1021
+ fontSize: 11,
1022
+ lineHeight: "16px",
1023
+ color: "var(--dsw-alias-label-secondary)"
1024
+ },
1025
+ muted: { color: "var(--dsw-alias-label-secondary)" },
1026
+ error: { color: "var(--dsw-alias-state-error-primary)" },
1027
+ metricGrid: {
1028
+ display: "grid",
1029
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 140px), 1fr))",
1030
+ gap: 10,
1031
+ marginBottom: 14
1032
+ },
1033
+ metric: {
1034
+ display: "flex",
1035
+ flexDirection: "column",
1036
+ gap: 2,
1037
+ minWidth: 0,
1038
+ padding: "10px 12px",
1039
+ borderRadius: 10,
1040
+ background: "var(--dsw-alias-bg-layer-2)"
1041
+ },
1042
+ metricLabel: {
1043
+ fontSize: 11,
1044
+ color: "var(--dsw-alias-label-secondary)"
1045
+ },
1046
+ metricValue: {
1047
+ fontSize: 17,
1048
+ lineHeight: "22px",
1049
+ fontWeight: 650,
1050
+ letterSpacing: "-0.02em",
1051
+ fontVariantNumeric: "tabular-nums"
1052
+ },
1053
+ usageBars: {
1054
+ display: "flex",
1055
+ alignItems: "flex-end",
1056
+ gap: 2,
1057
+ height: 96,
1058
+ padding: "0 1px"
1059
+ },
1060
+ usageBar: {
1061
+ flex: 1,
1062
+ minWidth: 2,
1063
+ height: "100%",
1064
+ display: "flex",
1065
+ alignItems: "flex-end"
1066
+ },
1067
+ usageBarFill: {
1068
+ width: "100%",
1069
+ minHeight: 2,
1070
+ borderRadius: 2,
1071
+ background: "var(--agnes-brand, #6C5CE7)"
1072
+ },
1073
+ usageAxis: {
1074
+ display: "flex",
1075
+ justifyContent: "space-between",
1076
+ gap: 12,
1077
+ marginTop: 6,
1078
+ fontSize: 11,
1079
+ color: "var(--dsw-alias-label-secondary)"
1080
+ },
1081
+ catalogueRow: {
1082
+ display: "flex",
1083
+ alignItems: "baseline",
1084
+ gap: 8,
1085
+ padding: "6px 0",
1086
+ borderBottom: "1px solid var(--dsw-alias-border-l1)",
1087
+ fontSize: 12
1088
+ },
1089
+ catalogueName: {
1090
+ flex: "0 0 auto",
1091
+ fontWeight: 500
1092
+ },
1093
+ catalogueLimits: {
1094
+ marginLeft: "auto",
1095
+ color: "var(--dsw-alias-label-secondary)",
1096
+ fontVariantNumeric: "tabular-nums",
1097
+ textAlign: "right"
1098
+ },
1099
+ note: {
1100
+ marginTop: 24,
1101
+ color: "var(--dsw-alias-label-secondary)",
1102
+ fontSize: 12,
1103
+ lineHeight: "18px"
1104
+ },
1105
+ empty: {
1106
+ color: "var(--dsw-alias-label-secondary)",
1107
+ padding: "18px 0"
1108
+ },
1109
+ field: {
1110
+ display: "flex",
1111
+ flexDirection: "column",
1112
+ gap: 6,
1113
+ marginBottom: 12
1114
+ },
1115
+ fieldLabel: {
1116
+ fontSize: 12,
1117
+ color: "var(--dsw-alias-label-secondary)"
1118
+ },
1119
+ input: {
1120
+ height: 32,
1121
+ padding: "0 10px",
1122
+ borderRadius: 8,
1123
+ fontSize: 13,
1124
+ border: "1px solid var(--dsw-alias-border-l2)",
1125
+ background: "var(--dsw-alias-bg-layer-1)",
1126
+ color: "var(--dsw-alias-label-primary)"
1127
+ },
1128
+ /** Real shell tokens — replaces the color-mix hack that faked "on-primary". */
1129
+ primary: {
1130
+ height: 32,
1131
+ padding: "0 16px",
1132
+ borderRadius: 8,
1133
+ fontSize: 13,
1134
+ fontWeight: 500,
1135
+ border: "1px solid var(--dsw-alias-border-l2)",
1136
+ background: "var(--dsw-alias-button-primary-fill)",
1137
+ color: "var(--dsw-alias-label-primary-foreground)",
1138
+ cursor: "pointer"
1139
+ },
1140
+ primaryHover: { background: "var(--dsw-alias-button-primary-hover)" },
1141
+ primaryBusy: {
1142
+ opacity: .6,
1143
+ cursor: "default"
1144
+ },
1145
+ formError: {
1146
+ color: "var(--dsw-alias-state-error-primary)",
1147
+ fontSize: 12,
1148
+ margin: "10px 0 0"
1149
+ },
1150
+ formNote: {
1151
+ color: "var(--dsw-alias-label-secondary)",
1152
+ fontSize: 12,
1153
+ margin: "10px 0 0"
1154
+ },
1155
+ rosterTools: {
1156
+ display: "flex",
1157
+ gap: 8,
1158
+ alignItems: "center",
1159
+ flexWrap: "wrap",
1160
+ marginBottom: 10
1161
+ },
1162
+ rosterCount: {
1163
+ fontSize: 12,
1164
+ color: "var(--dsw-alias-label-secondary)",
1165
+ fontVariantNumeric: "tabular-nums",
1166
+ marginLeft: "auto"
1167
+ },
1168
+ rosterBulk: {
1169
+ ...BUTTON,
1170
+ height: 32
1171
+ },
1172
+ modelList: {
1173
+ display: "flex",
1174
+ flexDirection: "column",
1175
+ margin: 0,
1176
+ padding: 0,
1177
+ listStyle: "none"
1178
+ },
1179
+ modelRow: {
1180
+ display: "flex",
1181
+ flexDirection: "column",
1182
+ gap: 2,
1183
+ padding: "8px 4px",
1184
+ borderBottom: "1px solid var(--dsw-alias-border-l1)"
1185
+ },
1186
+ modelRowHead: {
1187
+ display: "flex",
1188
+ alignItems: "center",
1189
+ gap: 8
1190
+ },
1191
+ modelRowOff: { opacity: .55 },
1192
+ modelCheck: {
1193
+ flex: "none",
1194
+ width: 15,
1195
+ height: 15,
1196
+ cursor: "pointer",
1197
+ accentColor: "var(--agnes-brand, #6C5CE7)",
1198
+ margin: 0
1199
+ },
1200
+ modelName: {
1201
+ flex: "0 1 auto",
1202
+ minWidth: 0,
1203
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1204
+ fontSize: 12,
1205
+ overflow: "hidden",
1206
+ textOverflow: "ellipsis",
1207
+ whiteSpace: "nowrap"
1208
+ },
1209
+ modelRate: {
1210
+ flex: "none",
1211
+ fontSize: 11,
1212
+ color: "var(--dsw-alias-label-secondary)",
1213
+ fontVariantNumeric: "tabular-nums"
1214
+ },
1215
+ modelBadge: {
1216
+ flex: "none",
1217
+ fontSize: 11,
1218
+ padding: "1px 7px",
1219
+ borderRadius: 999,
1220
+ background: "var(--dsw-alias-bg-layer-2)",
1221
+ color: "var(--dsw-alias-label-secondary)"
1222
+ },
1223
+ modelMeta: {
1224
+ paddingLeft: 25,
1225
+ fontSize: 11,
1226
+ lineHeight: "15px",
1227
+ color: "var(--dsw-alias-label-secondary)"
1228
+ },
1229
+ rosterFoot: {
1230
+ display: "flex",
1231
+ gap: 8,
1232
+ alignItems: "center",
1233
+ marginTop: 10
1234
+ },
1235
+ modelPanel: {
1236
+ border: "1px solid var(--dsw-alias-border-l1)",
1237
+ borderRadius: 12,
1238
+ background: "var(--dsw-alias-bg-layer-1)",
1239
+ padding: "12px 14px"
1240
+ }
1241
+ };
1242
+ }));
1243
+
1244
+ //#endregion
1245
+ //#region src/client/account-form.ts
1246
+ function AccountForm({ auth, onDone, tt, bare, hasSnapshot }) {
1247
+ const [username, setUsername] = useState("");
1248
+ const [password, setPassword] = useState("");
1249
+ const [showPassword, setShowPassword] = useState(false);
1250
+ const [busy, setBusy] = useState(false);
1251
+ const [formError, setFormError] = useState(null);
1252
+ const [formDetail, setFormDetail] = useState(null);
1253
+ const [saved, setSaved] = useState(false);
1254
+ const [forgotten, setForgotten] = useState(false);
1255
+ const [cooldownUntil, setCooldownUntil] = useState(0);
1256
+ const [now, setNow] = useState(() => Date.now());
1257
+ useEffect(() => {
1258
+ if (cooldownUntil <= Date.now()) return void 0;
1259
+ const timer = setInterval(() => {
1260
+ setNow(Date.now());
1261
+ if (Date.now() >= cooldownUntil) clearInterval(timer);
1262
+ }, 1e3);
1263
+ return () => clearInterval(timer);
1264
+ }, [cooldownUntil]);
1265
+ const cooling = now < cooldownUntil;
1266
+ const coolingMinutes = Math.max(1, Math.ceil((cooldownUntil - now) / 6e4));
1267
+ const setCooldown = useCallback((ms) => {
1268
+ setCooldownUntil(Date.now() + ms);
1269
+ setNow(Date.now());
1270
+ }, []);
1271
+ const submit = useCallback(async (event) => {
1272
+ event?.preventDefault?.();
1273
+ if (cooling) return;
1274
+ if (username.trim() === "" || password === "") {
1275
+ setFormError(tt("auth.empty"));
1276
+ return;
1277
+ }
1278
+ setBusy(true);
1279
+ setFormError(null);
1280
+ setFormDetail(null);
1281
+ try {
1282
+ const body = await postJson(ACCOUNT_PATH, {
1283
+ username: username.trim(),
1284
+ password
1285
+ });
1286
+ if (body && body.ok === true) {
1287
+ setPassword("");
1288
+ setSaved(true);
1289
+ setForgotten(false);
1290
+ onDone?.();
1291
+ return;
1292
+ }
1293
+ const code = body?.code;
1294
+ const waitMs = typeof body?.retryAfterMs === "number" ? body.retryAfterMs : null;
1295
+ if (waitMs !== null && waitMs > 0) {
1296
+ setCooldown(waitMs);
1297
+ setFormError(tt(code === "account_locked" ? "auth.locked" : "auth.rateLimited"));
1298
+ return;
1299
+ }
1300
+ if (typeof REFUSAL_TEXT[code] === "string") {
1301
+ setFormError(tt(REFUSAL_TEXT[code]));
1302
+ setFormDetail(typeof body?.detail === "string" && body.detail !== "" ? body.detail : null);
1303
+ return;
1304
+ }
1305
+ setFormError(code === "login_failed" ? format(tt("auth.failed"), { reason: body?.error ?? "" }) : body?.error ?? tt("auth.network"));
1306
+ } catch {
1307
+ setFormError(tt("auth.network"));
1308
+ } finally {
1309
+ setBusy(false);
1310
+ }
1311
+ }, [
1312
+ username,
1313
+ password,
1314
+ onDone,
1315
+ tt,
1316
+ cooling,
1317
+ setCooldown
1318
+ ]);
1319
+ const forget = useCallback(async () => {
1320
+ setBusy(true);
1321
+ setFormError(null);
1322
+ setFormDetail(null);
1323
+ try {
1324
+ const body = await postJson(ACCOUNT_PATH, { forget: true });
1325
+ if (body?.ok !== true) {
1326
+ setFormError(body?.error ?? tt("auth.network"));
1327
+ return;
1328
+ }
1329
+ setForgotten(true);
1330
+ setSaved(false);
1331
+ setUsername("");
1332
+ setPassword("");
1333
+ onDone?.();
1334
+ } catch {
1335
+ setFormError(tt("auth.network"));
1336
+ } finally {
1337
+ setBusy(false);
1338
+ }
1339
+ }, [onDone, tt]);
1340
+ return h("div", { style: bare ? {} : {
1341
+ ...S.card,
1342
+ maxWidth: 420
1343
+ } }, bare ? null : h("div", { style: S.sectionTitle }, tt("auth.title")), h("a", {
1344
+ href: AGNES_SIGNUP_URL,
1345
+ target: "_blank",
1346
+ rel: "noreferrer",
1347
+ style: {
1348
+ color: "var(--dsw-alias-label-primary)",
1349
+ fontSize: 12,
1350
+ marginTop: 10,
1351
+ display: "inline-block",
1352
+ textDecoration: "underline",
1353
+ cursor: "pointer"
1354
+ }
1355
+ }, tt(auth?.hasAccount ? "auth.portalHint" : "auth.registerHint")), h("form", { onSubmit: submit }, h("label", { style: S.field }, h("span", { style: S.fieldLabel }, tt("auth.username")), h("input", {
1356
+ style: S.input,
1357
+ value: username,
1358
+ autoComplete: "username",
1359
+ placeholder: tt("auth.placeholderUser"),
1360
+ disabled: busy,
1361
+ onChange: (event) => setUsername(event.target.value)
1362
+ })), h("label", { style: S.field }, h("span", { style: S.fieldLabel }, tt("auth.password")), h("div", { style: {
1363
+ display: "flex",
1364
+ gap: 6,
1365
+ alignItems: "center"
1366
+ } }, h("input", {
1367
+ style: {
1368
+ ...S.input,
1369
+ flex: 1
1370
+ },
1371
+ type: showPassword ? "text" : "password",
1372
+ value: password,
1373
+ autoComplete: "current-password",
1374
+ disabled: busy,
1375
+ onChange: (event) => setPassword(event.target.value)
1376
+ }), h("button", {
1377
+ type: "button",
1378
+ style: {
1379
+ ...S.button,
1380
+ flex: "none"
1381
+ },
1382
+ disabled: busy,
1383
+ onClick: () => setShowPassword((shown) => !shown)
1384
+ }, showPassword ? tt("auth.hide") : tt("auth.show")))), h("div", { style: {
1385
+ display: "flex",
1386
+ gap: 8,
1387
+ alignItems: "center",
1388
+ marginTop: 4
1389
+ } }, h("button", {
1390
+ type: "submit",
1391
+ style: {
1392
+ ...S.primary,
1393
+ ...busy || cooling ? S.primaryBusy : {}
1394
+ },
1395
+ disabled: busy || cooling
1396
+ }, busy ? tt("auth.submitting") : tt("auth.submit")), auth?.hasAccount ? h("button", {
1397
+ type: "button",
1398
+ style: S.button,
1399
+ disabled: busy,
1400
+ onClick: forget
1401
+ }, tt("auth.forget")) : null), forgotten ? h("p", {
1402
+ style: {
1403
+ ...S.formNote,
1404
+ color: "var(--dsw-alias-state-success-primary)"
1405
+ },
1406
+ role: "status"
1407
+ }, tt("auth.forgotten")) : saved && hasSnapshot !== true ? h("p", {
1408
+ style: {
1409
+ ...S.formNote,
1410
+ color: "var(--dsw-alias-state-success-primary)"
1411
+ },
1412
+ role: "status"
1413
+ }, tt("auth.working")) : null, formError ? h("p", {
1414
+ style: S.formError,
1415
+ role: "alert"
1416
+ }, formError) : null, formError && formDetail ? h("p", { style: S.formNote }, formDetail) : null, cooling ? h("p", { style: {
1417
+ ...S.formNote,
1418
+ color: "var(--dsw-alias-state-warn-primary)"
1419
+ } }, format(tt("auth.retryAfter"), { minutes: coolingMinutes })) : null, h("p", { style: S.formNote }, auth?.ephemeral === true ? tt("auth.ephemeral") : tt("auth.saved")), h("p", { style: S.formNote }, auth?.autoRecoverArmed === true ? tt("auth.autoRecoverOn") : tt("auth.autoRecoverOff"))));
1420
+ }
1421
+ var init_account_form = __esmMin((() => {
1422
+ init_const();
1423
+ init_format();
1424
+ init_http();
1425
+ init_runtime();
1426
+ init_snapshot();
1427
+ init_styles();
1428
+ }));
1429
+
1430
+ //#endregion
1431
+ //#region src/client/provider-controls.ts
1432
+ /**
1433
+ * The API-key card's status: ONLY key provenance (+ the ephemeral-host
1434
+ * warning). Registration state lives in `ProviderRegStatus` on the
1435
+ * provider card — after the panel split into one card per concern, a
1436
+ * line about registration inside the key card was information flying
1437
+ * across card boundaries, repeating what the provider card's own switch
1438
+ * and status already say.
1439
+ *
1440
+ * Hook-free on purpose: like the pool cards, it is exercised by the Node
1441
+ * render suite, so a reworded or dropped status line fails a check. It
1442
+ * renders ONLY from the snapshot's `llm` block, which never carries the
1443
+ * key itself — booleans and a source tag.
1444
+ */
1445
+ function ProviderStatus({ llm, tt }) {
1446
+ if (!llm || typeof llm !== "object") return null;
1447
+ const rows = [];
1448
+ const sourceText = llm.hasApiKey === true ? format(tt("llm.keyPresent"), { source: tt(`llm.src.${String(llm.keySource ?? "")}`) || String(llm.keySource ?? "") }) : tt("llm.noKey");
1449
+ rows.push(h("div", {
1450
+ style: {
1451
+ fontSize: 12,
1452
+ color: "var(--dsw-alias-label-secondary)"
1453
+ },
1454
+ role: "status"
1455
+ }, sourceText));
1456
+ if (llm.ephemeral === true) rows.push(h("div", { style: {
1457
+ ...S.formNote,
1458
+ color: "var(--dsw-alias-state-warn-primary)"
1459
+ } }, tt("llm.ephemeral")));
1460
+ return h("div", { style: {
1461
+ display: "flex",
1462
+ flexDirection: "column",
1463
+ gap: 6,
1464
+ marginBottom: 12
1465
+ } }, ...rows);
1466
+ }
1467
+ /**
1468
+ * The provider card's registration status, hook-free like `ProviderStatus`
1469
+ * so the render suite pins every branch. The card title and the switch
1470
+ * below say "registration" already, so this is pure state + counts: the
1471
+ * id rides inside whichever line is showing, never as its own row.
1472
+ */
1473
+ function ProviderRegStatus({ llm, tt }) {
1474
+ if (!llm || typeof llm !== "object") return null;
1475
+ if (llm.registerProvider === true && llm.providerRegistered === true) return h("div", {
1476
+ style: {
1477
+ fontSize: 12,
1478
+ color: "var(--dsw-alias-label-secondary)"
1479
+ },
1480
+ role: "status"
1481
+ }, format(tt("llm.registered"), {
1482
+ id: String(llm.providerId ?? ""),
1483
+ models: count(llm.modelCount),
1484
+ vision: count(llm.visionCount)
1485
+ }));
1486
+ if (llm.registerProvider === true && typeof llm.providerError === "string" && llm.providerError !== "") return h("div", {
1487
+ style: S.formError,
1488
+ role: "alert"
1489
+ }, format(tt("llm.error"), { error: llm.providerError }));
1490
+ if (llm.registerProvider === true && llm.llmAvailable !== true) return h("div", { style: {
1491
+ ...S.formNote,
1492
+ color: "var(--dsw-alias-state-warn-primary)"
1493
+ } }, tt("llm.noService"));
1494
+ if (llm.registerProvider === true) return h("div", {
1495
+ style: {
1496
+ ...S.muted,
1497
+ fontSize: 12
1498
+ },
1499
+ role: "status"
1500
+ }, format(tt("llm.registeredPending"), { id: String(llm.providerId ?? "") }));
1501
+ return h("div", { style: {
1502
+ ...S.muted,
1503
+ fontSize: 12
1504
+ } }, format(tt("llm.off"), { id: String(llm.providerId ?? "") }));
1505
+ }
1506
+ /**
1507
+ * The live provider-registration switch (docs/PROVIDER-HOT-RELOAD.md).
1508
+ *
1509
+ * Posts `{ enabled }` to the plugin's own `/provider` route; the Host
1510
+ * persists the value in its state file and republishes the adapter pair
1511
+ * on the same request, so the flip lands without a config edit or a
1512
+ * restart. Hook-based like `ApiKeyForm`, so the render suite (which
1513
+ * cannot mount hooks) exercises the secret-free status lines instead;
1514
+ * the route itself is covered by `routes.test.mjs`. The state shown is
1515
+ * the SNAPSHOT's effective value, never local optimism — the poll after
1516
+ * `onDone` repaints whatever the Host actually reports.
1517
+ */
1518
+ function ProviderSwitch({ llm, onDone, tt }) {
1519
+ const [busy, setBusy] = useState(false);
1520
+ const [switchError, setSwitchError] = useState(null);
1521
+ const enabled = llm?.registerProvider === true;
1522
+ const toggle = useCallback(async () => {
1523
+ setBusy(true);
1524
+ setSwitchError(null);
1525
+ try {
1526
+ await postJsonOrThrow(PROVIDER_PATH, { enabled: !enabled });
1527
+ onDone?.();
1528
+ } catch (error) {
1529
+ setSwitchError(format(tt("llm.switchError"), { error: error instanceof Error ? error.message : String(error) }));
1530
+ } finally {
1531
+ setBusy(false);
1532
+ }
1533
+ }, [
1534
+ enabled,
1535
+ onDone,
1536
+ tt
1537
+ ]);
1538
+ return h("label", { style: {
1539
+ display: "flex",
1540
+ gap: 8,
1541
+ alignItems: "center",
1542
+ margin: "0 0 12px",
1543
+ cursor: busy ? "wait" : "pointer"
1544
+ } }, h("input", {
1545
+ type: "checkbox",
1546
+ checked: enabled,
1547
+ disabled: busy,
1548
+ onChange: toggle
1549
+ }), h("span", { style: {
1550
+ fontSize: 12,
1551
+ color: "var(--dsw-alias-label-secondary)"
1552
+ } }, busy ? tt("llm.switchBusy") : tt("llm.switch")), switchError ? h("span", {
1553
+ style: S.formError,
1554
+ role: "alert"
1555
+ }, switchError) : null);
1556
+ }
1557
+ /**
1558
+ * The shared body of an agent-tool card (`DrawSwitch`, `VideoSwitch`).
1559
+ *
1560
+ * Both tools are mounted by the SAME Host ladder (`mountAgentTool` in
1561
+ * `lifecycle.ts`) and both routes are served by the SAME handler shape
1562
+ * (`registerToolSwitchRoute` in `routes.ts`), so their cards differ only in
1563
+ * which route they post to, which `llm.*` fields they read, and which
1564
+ * dictionary prefix labels them. Writing the card once is what keeps the two
1565
+ * from drifting into different states for the same condition — the failure
1566
+ * mode the Host-side refactor was done to prevent.
1567
+ *
1568
+ * The dictionary suffixes are identical under both prefixes (`switch`,
1569
+ * `switchBusy`, `switchError`, `onList`, `off`, `needsKey`, `noCandidates`,
1570
+ * `badge`, `badgeNone`, `autoOption`, `effective`, `title`), so `k()` is the
1571
+ * only translation step.
1572
+ *
1573
+ * Mountable: the stand-in React returns `useState`'s initial value and passes
1574
+ * `useCallback` through, so `render.test.mjs` pins the picker's row markup —
1575
+ * the `modelRowHead` wrap below is that contract.
1576
+ */
1577
+ function ToolSwitch({ path, prefix, modelKey, enabled, hasKey, candidates, preferred, effective, extraNote, onDone, tt }) {
1578
+ const [busy, setBusy] = useState(false);
1579
+ const [switchError, setSwitchError] = useState(null);
1580
+ const k = (suffix) => tt(`${prefix}.${suffix}`);
1581
+ const toggle = useCallback(async () => {
1582
+ setBusy(true);
1583
+ setSwitchError(null);
1584
+ try {
1585
+ await postJsonOrThrow(path, { enabled: !enabled });
1586
+ onDone?.();
1587
+ } catch (error) {
1588
+ setSwitchError(format(k("switchError"), { error: error instanceof Error ? error.message : String(error) }));
1589
+ } finally {
1590
+ setBusy(false);
1591
+ }
1592
+ }, [
1593
+ path,
1594
+ prefix,
1595
+ enabled,
1596
+ onDone,
1597
+ tt
1598
+ ]);
1599
+ const saveModel = useCallback(async (id) => {
1600
+ setBusy(true);
1601
+ setSwitchError(null);
1602
+ try {
1603
+ await postJsonOrThrow(path, { [modelKey]: id });
1604
+ onDone?.();
1605
+ } catch (error) {
1606
+ setSwitchError(format(k("switchError"), { error: error instanceof Error ? error.message : String(error) }));
1607
+ } finally {
1608
+ setBusy(false);
1609
+ }
1610
+ }, [
1611
+ path,
1612
+ modelKey,
1613
+ prefix,
1614
+ onDone,
1615
+ tt
1616
+ ]);
1617
+ const statusText = enabled ? hasKey ? k("onList") : k("needsKey") : k("off");
1618
+ const radioName = `${prefix}-model`;
1619
+ const pickerRows = hasKey && candidates.length > 0 ? h("ul", {
1620
+ style: S.modelList,
1621
+ role: "radiogroup",
1622
+ "aria-label": k("title")
1623
+ }, h("li", {
1624
+ style: enabled ? S.modelRow : {
1625
+ ...S.modelRow,
1626
+ ...S.modelRowOff
1627
+ },
1628
+ key: "auto"
1629
+ }, h("div", { style: S.modelRowHead }, h("label", { style: {
1630
+ display: "flex",
1631
+ alignItems: "center",
1632
+ gap: 10,
1633
+ flex: "1 1 auto",
1634
+ minWidth: 0,
1635
+ cursor: busy ? "default" : "pointer"
1636
+ } }, h("input", {
1637
+ type: "radio",
1638
+ name: radioName,
1639
+ checked: preferred === null && enabled,
1640
+ disabled: busy || !enabled,
1641
+ onChange: () => void saveModel(null),
1642
+ style: S.modelCheck
1643
+ }), h("span", { style: S.modelName }, k("autoOption"))), h("span", { style: S.modelBadge }, effective !== "" ? `${k("badge")} · ${effective}` : `${k("badge")} · ${k("badgeNone")}`))), ...candidates.map((id) => h("li", {
1644
+ style: enabled ? S.modelRow : {
1645
+ ...S.modelRow,
1646
+ ...S.modelRowOff
1647
+ },
1648
+ key: id
1649
+ }, h("div", { style: S.modelRowHead }, h("label", { style: {
1650
+ display: "flex",
1651
+ alignItems: "center",
1652
+ gap: 10,
1653
+ flex: "1 1 auto",
1654
+ minWidth: 0,
1655
+ cursor: busy ? "default" : "pointer"
1656
+ } }, h("input", {
1657
+ type: "radio",
1658
+ name: radioName,
1659
+ checked: preferred === id && enabled,
1660
+ disabled: busy || !enabled,
1661
+ onChange: () => void saveModel(id),
1662
+ style: S.modelCheck
1663
+ }), h("span", {
1664
+ style: S.modelName,
1665
+ title: id
1666
+ }, id)), h("span", { style: S.modelBadge }, effective === id && enabled ? `${k("badge")} · ${k("effective")}` : k("badge")))))) : null;
1667
+ const noListHint = hasKey && candidates.length === 0 ? h("div", { style: {
1668
+ ...S.muted,
1669
+ fontSize: 12,
1670
+ marginTop: 4
1671
+ } }, k("noCandidates")) : null;
1672
+ return h("div", { style: { marginBottom: 12 } }, h("label", { style: {
1673
+ display: "flex",
1674
+ gap: 8,
1675
+ alignItems: "baseline",
1676
+ cursor: busy ? "wait" : "pointer"
1677
+ } }, h("input", {
1678
+ type: "checkbox",
1679
+ checked: enabled,
1680
+ disabled: busy,
1681
+ onChange: toggle
1682
+ }), h("span", { style: {
1683
+ fontSize: 12,
1684
+ color: "var(--dsw-alias-label-primary)"
1685
+ } }, busy ? k("switchBusy") : k("switch"))), h("div", { style: {
1686
+ ...S.muted,
1687
+ fontSize: 12,
1688
+ marginTop: 4
1689
+ } }, statusText), pickerRows, noListHint, extraNote ?? null, switchError ? h("div", {
1690
+ style: S.formError,
1691
+ role: "alert"
1692
+ }, switchError) : null);
1693
+ }
1694
+ /** Ids off a snapshot array field, coerced to strings and guarded against `null`. */
1695
+ function idList(value) {
1696
+ return Array.isArray(value) ? value.map((id) => String(id)) : [];
1697
+ }
1698
+ /**
1699
+ * The live draw-tool switch (docs/PROVIDER-HOT-RELOAD.md, same discipline
1700
+ * as `ProviderSwitch`). Posts `{ enabled }` / `{ modelId }` to the plugin's
1701
+ * own `/draw` route; the Host persists the value in its state file. The draw
1702
+ * tool itself is mounted at `apply` time (lifecycle.ts), so a panel flip only
1703
+ * becomes visible after the NEXT Host (re)mount — but the switch state, the
1704
+ * source, and the snapshot's `llm.drawEnabled` are all live, so the panel
1705
+ * shows the effective value immediately. The route is covered by
1706
+ * `routes.test.mjs`.
1707
+ */
1708
+ function DrawSwitch({ llm, onDone, tt }) {
1709
+ return h(ToolSwitch, {
1710
+ path: DRAW_PATH,
1711
+ prefix: "draw",
1712
+ modelKey: "drawModelId",
1713
+ enabled: llm?.drawEnabled === true,
1714
+ hasKey: llm?.hasApiKey === true,
1715
+ candidates: idList(llm?.drawCandidateIds),
1716
+ preferred: llm?.drawPreferredModel != null ? String(llm.drawPreferredModel) : null,
1717
+ effective: String(llm?.drawModel ?? ""),
1718
+ onDone,
1719
+ tt
1720
+ });
1721
+ }
1722
+ /**
1723
+ * The live video-tool switch — `DrawSwitch`'s twin, on `/video`.
1724
+ *
1725
+ * The one thing it says that the draw card does not: the catalogue mixes two
1726
+ * video PARAMETER families. `videoCandidateIds` lists ALL of them (the tool
1727
+ * drives both), and `video25ModelIds` names the 2.5-series subset, whose body
1728
+ * schema (`mode`/`seconds`/`size`/`aspect_ratio`) is disjoint from V2.0's
1729
+ * (`width`/`height`/`num_frames`/`frame_rate`). The tool adapts automatically:
1730
+ * when a 2.5 model is selected it builds the 2.5 body (whole seconds,
1731
+ * resolution tier, aspect whitelist) and translates any frame fields to the
1732
+ * nearest whole second. The card's note line says so, rather than leaving a
1733
+ * reader wondering why a seconds-based model sits next to a frame-based one.
1734
+ */
1735
+ function VideoSwitch({ llm, onDone, tt }) {
1736
+ const excluded = idList(llm?.video25ModelIds);
1737
+ const extraNote = excluded.length > 0 ? h("div", { style: {
1738
+ ...S.muted,
1739
+ fontSize: 12,
1740
+ marginTop: 4
1741
+ } }, format(tt("video.note25"), {
1742
+ count: count(excluded.length),
1743
+ ids: excluded.join(" · ")
1744
+ })) : null;
1745
+ return h(ToolSwitch, {
1746
+ path: VIDEO_PATH,
1747
+ prefix: "video",
1748
+ modelKey: "videoModelId",
1749
+ enabled: llm?.videoEnabled === true,
1750
+ hasKey: llm?.hasApiKey === true,
1751
+ candidates: idList(llm?.videoCandidateIds),
1752
+ preferred: llm?.videoPreferredModel != null ? String(llm.videoPreferredModel) : null,
1753
+ effective: String(llm?.videoModel ?? ""),
1754
+ extraNote,
1755
+ onDone,
1756
+ tt
1757
+ });
1758
+ }
1759
+ var init_provider_controls = __esmMin((() => {
1760
+ init_const();
1761
+ init_format();
1762
+ init_http();
1763
+ init_runtime();
1764
+ init_styles();
1765
+ }));
1766
+
1767
+ //#endregion
1768
+ //#region src/client/models.ts
1769
+ /** The model ids a roster advertises, junk entries dropped. */
1770
+ function rosterIds(roster) {
1771
+ return (Array.isArray(roster) ? roster : []).filter((model) => typeof model === "string" && model !== "");
1772
+ }
1773
+ /**
1774
+ * Whether one model id is offered by an allow-list.
1775
+ *
1776
+ * Mirrors the Host's `filterByEnabled`: an empty list offers everything,
1777
+ * a non-empty one is a strict allow-list, and `HIDE_ALL_MODELS` alone
1778
+ * offers nothing.
1779
+ */
1780
+ function modelIsOn(enabledIds, id) {
1781
+ const list = Array.isArray(enabledIds) ? enabledIds : [];
1782
+ return list.length === 0 ? true : list.includes(id);
1783
+ }
1784
+ /**
1785
+ * The allow-list that offers exactly the ids in `on`.
1786
+ *
1787
+ * Every mutation funnels through here, so the two extreme spellings are
1788
+ * emitted consistently: an empty list (nothing curated, every model
1789
+ * offered) and `HIDE_ALL_MODELS` alone (nothing offered). No caller can
1790
+ * post a list the Host would read differently than the picker shows.
1791
+ */
1792
+ function allowListFor(on, roster) {
1793
+ const all = rosterIds(roster);
1794
+ const kept = all.filter((model) => on.has(model));
1795
+ if (kept.length === 0) return [HIDE_ALL_MODELS];
1796
+ if (kept.length === all.length) return [];
1797
+ return kept;
1798
+ }
1799
+ /**
1800
+ * The next allow-list after ticking or unticking one model.
1801
+ *
1802
+ * The result is computed against the WHOLE roster, not the current
1803
+ * list: the saved value is a complete allow-list rather than a diff, so
1804
+ * a curated catalogue stays curated when the catalogue later grows —
1805
+ * new models start unticked instead of slipping into DSH on their own.
1806
+ */
1807
+ function toggleModelIn(enabledIds, roster, id) {
1808
+ const on = new Set(rosterIds(roster).filter((model) => modelIsOn(enabledIds, model)));
1809
+ if (on.has(id)) on.delete(id);
1810
+ else on.add(id);
1811
+ return allowListFor(on, roster);
1812
+ }
1813
+ /** The allow-list for a bulk "tick all" / "untick all". */
1814
+ function setAllModelsIn(roster, allOn) {
1815
+ return allowListFor(new Set(allOn ? rosterIds(roster) : []), roster);
1816
+ }
1817
+ /**
1818
+ * The next allow-list after a bulk "tick all" / "untick all" over one set
1819
+ * of targets, computed against the WHOLE roster.
1820
+ *
1821
+ * The targets are the ids the reader is looking at right now (a filtered
1822
+ * view); the rows outside them keep whatever the Host already offers, so
1823
+ * the result stays a complete allow-list rather than a diff. Both id lists
1824
+ * must be STRINGS — `rosterIds` drops anything that is not one, so a roster
1825
+ * of `{id}` rows would filter to nothing and the whole call would collapse
1826
+ * to the hide-all sentinel no matter which way the button was pressed.
1827
+ */
1828
+ function bulkModelsIn(enabledIds, roster, targets, allOn) {
1829
+ const on = new Set(rosterIds(roster).filter((model) => modelIsOn(enabledIds, model)));
1830
+ for (const id of targets) if (allOn) on.add(id);
1831
+ else on.delete(id);
1832
+ return allowListFor(on, roster);
1833
+ }
1834
+ var HIDE_ALL_MODELS;
1835
+ var init_models = __esmMin((() => {
1836
+ HIDE_ALL_MODELS = "__hide_all__";
1837
+ }));
1838
+
1839
+ //#endregion
1840
+ //#region src/client/model-picker.ts
1841
+ /**
1842
+ * The model picker's row list - hook-free, so the Node render suite
1843
+ * drives the very rows the browser draws.
1844
+ *
1845
+ * Each row is two lines in the WorkBuddy shape: a head line (checkbox, the
1846
+ * model name, an optional `×N` pseudo rate, badges for NOTABLE states only)
1847
+ * and an indented parameter line quoting the figures the platform declares -
1848
+ * window, output ceiling, and the thinking levels DSH's selector will really
1849
+ * offer for THIS model. A provider-wide constant (the default effort) never
1850
+ * repeats per row - it is stated once in the header, because a fact that
1851
+ * never varies between rows is noise, not information.
1852
+ * The rows come only from the Host's roster, so a curated id that no longer
1853
+ * exists can never become a checkbox: curation is a filter over the catalogue,
1854
+ * never a catalogue of its own. A default ("text only") earns no badge, and a
1855
+ * figure the catalogue does not declare draws no segment - the list quotes
1856
+ * facts, never guesses.
1857
+ */
1858
+ function ModelRoster({ models, enabledIds, busy, tt, onToggle }) {
1859
+ const list = Array.isArray(models) ? models : [];
1860
+ return h("ul", {
1861
+ style: S.modelList,
1862
+ role: "list"
1863
+ }, list.map((model) => {
1864
+ const id = String(model?.id ?? "");
1865
+ const label = String(model?.name ?? id);
1866
+ const on = modelIsOn(enabledIds, id);
1867
+ const meta = [
1868
+ typeof model?.contextWindow === "number" && model.contextWindow > 0 ? format(tt("llm.contextBadge"), { ctx: tokenSize(model.contextWindow) }) : null,
1869
+ typeof model?.maxOutputLength === "number" && model.maxOutputLength > 0 ? format(tt("llm.metaOutput"), { out: tokenSize(model.maxOutputLength) }) : null,
1870
+ Array.isArray(model?.thinkingLevels) && model.thinkingLevels.length > 0 ? format(tt("llm.metaLevels"), { levels: model.thinkingLevels.map((level) => tt(`llm.level.${level}`)).join("/") }) : null
1871
+ ].filter(Boolean).join(" · ");
1872
+ const rate = typeof model?.multiplier === "number" ? model.multiplier : null;
1873
+ return h("li", {
1874
+ key: id,
1875
+ style: {
1876
+ ...S.modelRow,
1877
+ ...on ? {} : S.modelRowOff
1878
+ }
1879
+ }, h("div", { style: S.modelRowHead }, h("label", { style: {
1880
+ display: "flex",
1881
+ alignItems: "center",
1882
+ gap: 10,
1883
+ flex: "1 1 auto",
1884
+ minWidth: 0,
1885
+ cursor: busy ? "default" : "pointer"
1886
+ } }, h("input", {
1887
+ type: "checkbox",
1888
+ checked: on,
1889
+ disabled: busy === true,
1890
+ style: S.modelCheck,
1891
+ "aria-label": label,
1892
+ onChange: onToggle ? () => onToggle(id) : void 0
1893
+ }), h("span", {
1894
+ style: S.modelName,
1895
+ title: id
1896
+ }, label), rate !== null ? h("span", {
1897
+ style: S.modelRate,
1898
+ title: tt("llm.rosterRateTitle")
1899
+ }, `×${rate}`) : null), model?.vision === true ? h("span", { style: S.modelBadge }, tt("llm.rosterVision")) : null, model?.quotaExhausted === true ? h("span", { style: {
1900
+ ...S.modelBadge,
1901
+ color: "var(--dsw-alias-state-error-primary)"
1902
+ } }, tt("llm.rosterExhausted")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
1903
+ }));
1904
+ }
1905
+ /**
1906
+ * The curated model allow-list: which of this key's models get pushed to
1907
+ * DSH's model list.
1908
+ *
1909
+ * Hook-based like `ApiKeyForm`, so the render suite exercises the secret-
1910
+ * free half it draws - `ModelRoster` and the counts - instead of this
1911
+ * state machine. The edit is local until saved: the picker holds a draft
1912
+ * of the allow-list, the "unsaved" state is DERIVED by comparing it with
1913
+ * the Host's current value, and the "saved" state is the same comparison
1914
+ * after a poll echoes the write. Both therefore cannot lie: a save that
1915
+ * never reached the Host keeps showing the edits, and an edit that ends
1916
+ * up identical to the Host's value shows neither button.
1917
+ */
1918
+ function ModelPicker({ llm, onDone, tt }) {
1919
+ const models = Array.isArray(llm?.models) ? llm.models : [];
1920
+ const hostIds = Array.isArray(llm?.enabledModelIds) ? llm.enabledModelIds : [];
1921
+ const [ids, setIds] = useState(() => hostIds.slice());
1922
+ const [busy, setBusy] = useState(false);
1923
+ const [query, setQuery] = useState("");
1924
+ const [savedKey, setSavedKey] = useState(null);
1925
+ const [notice, setNotice] = useState(null);
1926
+ const hostKey = useMemo(() => JSON.stringify(hostIds), [hostIds]);
1927
+ const dirty = useMemo(() => JSON.stringify(ids), [ids]) !== hostKey;
1928
+ const justSaved = savedKey !== null && savedKey === hostKey;
1929
+ useEffect(() => {
1930
+ if (dirty === false) setIds(hostIds);
1931
+ }, [hostKey]);
1932
+ useEffect(() => {
1933
+ if (dirty === true) setSavedKey(null);
1934
+ }, [dirty]);
1935
+ const save = useCallback(async () => {
1936
+ if (busy) return;
1937
+ setBusy(true);
1938
+ setNotice(null);
1939
+ const posted = JSON.stringify(ids);
1940
+ try {
1941
+ await postJsonOrThrow(MODELS_PATH, { enabledModelIds: ids });
1942
+ setSavedKey(posted);
1943
+ onDone?.();
1944
+ } catch (error) {
1945
+ setNotice(format(tt("llm.rosterError"), { error: error instanceof Error ? error.message : String(error) }));
1946
+ } finally {
1947
+ setBusy(false);
1948
+ }
1949
+ }, [
1950
+ busy,
1951
+ ids,
1952
+ onDone,
1953
+ tt
1954
+ ]);
1955
+ const needle = query.trim().toLowerCase();
1956
+ const visible = useMemo(() => models.filter((model) => {
1957
+ if (needle === "") return true;
1958
+ return String(model?.id ?? "").toLowerCase().includes(needle) || String(model?.name ?? "").toLowerCase().includes(needle);
1959
+ }), [needle, models]);
1960
+ const tickedCount = visible.filter((model) => modelIsOn(ids, String(model?.id ?? ""))).length;
1961
+ /** Apply "tick all" / "untick all" to the VISIBLE rows only. */
1962
+ const bulk = useCallback((allOn) => {
1963
+ const roster = models.map((model) => String(model?.id ?? ""));
1964
+ const targets = visible.map((model) => String(model?.id ?? ""));
1965
+ setIds(bulkModelsIn(ids, roster, targets, allOn));
1966
+ setNotice(null);
1967
+ }, [
1968
+ models,
1969
+ visible,
1970
+ ids
1971
+ ]);
1972
+ return h("div", { style: { marginBottom: 14 } }, h("p", { style: { margin: "0 0 10px" } }, h("span", { style: S.sectionTitle }, tt("llm.roster"), " — "), h("span", { style: {
1973
+ ...S.muted,
1974
+ fontSize: 12
1975
+ } }, tt("llm.rosterHint")), typeof llm?.thinkingDefault === "string" && llm.thinkingDefault !== "" ? h("span", { style: {
1976
+ ...S.muted,
1977
+ fontSize: 12
1978
+ } }, ` · ${format(tt("llm.rosterThinkingDefault"), { level: tt(`llm.level.${llm.thinkingDefault}`) })}`) : null), models.length === 0 ? h("p", { style: S.empty }, tt("llm.rosterEmpty")) : h("div", null, h("div", { style: S.rosterTools }, h("input", {
1979
+ type: "search",
1980
+ style: {
1981
+ ...S.input,
1982
+ flex: "1 1 200px",
1983
+ width: "auto"
1984
+ },
1985
+ value: query,
1986
+ placeholder: tt("llm.rosterSearchPlaceholder"),
1987
+ "aria-label": tt("llm.rosterSearchPlaceholder"),
1988
+ autoComplete: "off",
1989
+ name: "model-search",
1990
+ disabled: busy,
1991
+ onChange: (event) => setQuery(event.target.value)
1992
+ }), h("span", {
1993
+ style: S.rosterCount,
1994
+ title: format(tt("llm.rosterCount"), {
1995
+ selected: tickedCount,
1996
+ total: visible.length
1997
+ })
1998
+ }, format(tt("llm.rosterCount"), {
1999
+ selected: tickedCount,
2000
+ total: visible.length
2001
+ })), h("button", {
2002
+ type: "button",
2003
+ style: S.rosterBulk,
2004
+ disabled: busy === true || visible.length === 0,
2005
+ onClick: () => bulk(true)
2006
+ }, tt("llm.rosterAll")), h("button", {
2007
+ type: "button",
2008
+ style: S.rosterBulk,
2009
+ disabled: busy === true || visible.length === 0,
2010
+ onClick: () => bulk(false)
2011
+ }, tt("llm.rosterNone"))), visible.length === 0 ? h("p", { style: S.empty }, tt("llm.rosterNoMatch")) : h(ModelRoster, {
2012
+ models: visible,
2013
+ enabledIds: ids,
2014
+ busy,
2015
+ tt,
2016
+ onToggle: (id) => {
2017
+ setIds(toggleModelIn(ids, models.map((model) => String(model?.id ?? "")), id));
2018
+ setNotice(null);
2019
+ }
2020
+ }), dirty ? h("div", { style: S.rosterFoot }, h("button", {
2021
+ type: "button",
2022
+ style: S.primary,
2023
+ disabled: busy === true,
2024
+ onClick: () => void save()
2025
+ }, busy ? tt("llm.rosterSaving") : tt("llm.rosterSave")), h("button", {
2026
+ type: "button",
2027
+ style: S.button,
2028
+ disabled: busy === true,
2029
+ onClick: () => {
2030
+ setIds(hostIds);
2031
+ setNotice(null);
2032
+ }
2033
+ }, tt("llm.rosterDiscard")), h("span", { style: {
2034
+ ...S.muted,
2035
+ fontSize: 12
2036
+ } }, tt("llm.rosterUnsaved"))) : justSaved ? h("p", {
2037
+ style: {
2038
+ ...S.formNote,
2039
+ color: "var(--dsw-alias-state-success-primary)"
2040
+ },
2041
+ role: "status"
2042
+ }, tt("llm.rosterSaved")) : null, notice !== null ? h("p", {
2043
+ style: S.formError,
2044
+ role: "alert"
2045
+ }, notice) : null));
2046
+ }
2047
+ var init_model_picker = __esmMin((() => {
2048
+ init_const();
2049
+ init_format();
2050
+ init_http();
2051
+ init_models();
2052
+ init_runtime();
2053
+ init_styles();
2054
+ }));
2055
+
2056
+ //#endregion
2057
+ //#region src/client/api-key-form.ts
2058
+ function ApiKeyForm({ llm, onDone, tt }) {
2059
+ const [apiKey, setApiKey] = useState("");
2060
+ const [showKey, setShowKey] = useState(false);
2061
+ const [busy, setBusy] = useState(false);
2062
+ const [formError, setFormError] = useState(null);
2063
+ const [saved, setSaved] = useState(false);
2064
+ const [forgotten, setForgotten] = useState(false);
2065
+ const post = useCallback((payload) => postJson(API_KEY_PATH, payload), []);
2066
+ const submit = useCallback(async (event) => {
2067
+ event?.preventDefault?.();
2068
+ if (apiKey.trim() === "") {
2069
+ setFormError(tt("llm.empty"));
2070
+ return;
2071
+ }
2072
+ setBusy(true);
2073
+ setFormError(null);
2074
+ try {
2075
+ const body = await post({ apiKey });
2076
+ if (body && body.ok === true) {
2077
+ setApiKey("");
2078
+ setSaved(true);
2079
+ setForgotten(false);
2080
+ onDone?.();
2081
+ return;
2082
+ }
2083
+ setFormError(body?.error ?? tt("auth.network"));
2084
+ } catch {
2085
+ setFormError(tt("auth.network"));
2086
+ } finally {
2087
+ setBusy(false);
2088
+ }
2089
+ }, [
2090
+ apiKey,
2091
+ post,
2092
+ onDone,
2093
+ tt
2094
+ ]);
2095
+ const forget = useCallback(async () => {
2096
+ setBusy(true);
2097
+ setFormError(null);
2098
+ try {
2099
+ const body = await post({ forget: true });
2100
+ if (body?.ok !== true) {
2101
+ setFormError(body?.error ?? tt("auth.network"));
2102
+ return;
2103
+ }
2104
+ setForgotten(true);
2105
+ setSaved(false);
2106
+ setApiKey("");
2107
+ onDone?.();
2108
+ } catch {
2109
+ setFormError(tt("auth.network"));
2110
+ } finally {
2111
+ setBusy(false);
2112
+ }
2113
+ }, [
2114
+ post,
2115
+ onDone,
2116
+ tt
2117
+ ]);
2118
+ const canForget = llm?.hasApiKey === true && llm?.keySource === "credentials";
2119
+ const keyEditor = h("div", null, h("label", { style: S.field }, h("span", { style: S.fieldLabel }, tt("llm.keyField")), h("div", { style: {
2120
+ display: "flex",
2121
+ gap: 6,
2122
+ alignItems: "center"
2123
+ } }, h("input", {
2124
+ style: {
2125
+ ...S.input,
2126
+ flex: 1
2127
+ },
2128
+ type: showKey ? "text" : "password",
2129
+ value: apiKey,
2130
+ autoComplete: "off",
2131
+ placeholder: tt("llm.placeholder"),
2132
+ disabled: busy,
2133
+ onChange: (event) => setApiKey(event.target.value)
2134
+ }), h("button", {
2135
+ type: "button",
2136
+ style: {
2137
+ ...S.button,
2138
+ flex: "none"
2139
+ },
2140
+ disabled: busy,
2141
+ onClick: () => setShowKey((shown) => !shown)
2142
+ }, showKey ? tt("auth.hide") : tt("auth.show")))), h("div", { style: {
2143
+ display: "flex",
2144
+ gap: 8,
2145
+ alignItems: "center",
2146
+ marginTop: 4
2147
+ } }, h("button", {
2148
+ type: "submit",
2149
+ style: {
2150
+ ...S.primary,
2151
+ ...busy ? S.primaryBusy : {}
2152
+ },
2153
+ disabled: busy
2154
+ }, busy ? tt("llm.saving") : tt("llm.save")), canForget ? h("button", {
2155
+ type: "button",
2156
+ style: S.button,
2157
+ disabled: busy,
2158
+ onClick: forget
2159
+ }, tt("llm.forget")) : null), forgotten ? h("p", {
2160
+ style: {
2161
+ ...S.formNote,
2162
+ color: "var(--dsw-alias-state-success-primary)"
2163
+ },
2164
+ role: "status"
2165
+ }, tt("llm.forgotten")) : saved ? h("p", {
2166
+ style: {
2167
+ ...S.formNote,
2168
+ color: "var(--dsw-alias-state-success-primary)"
2169
+ },
2170
+ role: "status"
2171
+ }, tt("llm.saved")) : null, formError ? h("p", {
2172
+ style: S.formError,
2173
+ role: "alert"
2174
+ }, formError) : null, h("p", { style: S.formNote }, tt("llm.footnote")), llm?.hasApiKey !== true ? h("a", {
2175
+ href: AGNES_SIGNUP_URL,
2176
+ target: "_blank",
2177
+ rel: "noreferrer",
2178
+ style: {
2179
+ color: "var(--dsw-alias-label-primary)",
2180
+ fontSize: 12,
2181
+ marginTop: 10,
2182
+ display: "inline-block",
2183
+ textDecoration: "underline",
2184
+ cursor: "pointer"
2185
+ }
2186
+ }, tt("llm.keyRegisterHint")) : null);
2187
+ return h("form", { onSubmit: submit }, keyEditor, h(ProviderStatus, {
2188
+ llm,
2189
+ tt
2190
+ }));
2191
+ }
2192
+ /**
2193
+ * The provider-registration half — the live switch plus its "which models
2194
+ * get pushed" roster — as ONE card body. Split from `ApiKeyForm` when the
2195
+ * panel grew one SectionCard per concern: key, provider+push, draw are
2196
+ * three different functions and no longer share a card.
2197
+ */
2198
+ function ProviderForm({ llm, onDone, tt }) {
2199
+ return h("div", null, h(ProviderSwitch, {
2200
+ llm,
2201
+ onDone,
2202
+ tt
2203
+ }), h(ModelPicker, {
2204
+ llm,
2205
+ onDone,
2206
+ tt
2207
+ }), h(ProviderRegStatus, {
2208
+ llm,
2209
+ tt
2210
+ }));
2211
+ }
2212
+ var init_api_key_form = __esmMin((() => {
2213
+ init_const();
2214
+ init_http();
2215
+ init_runtime();
2216
+ init_provider_controls();
2217
+ init_model_picker();
2218
+ init_styles();
2219
+ }));
2220
+
2221
+ //#endregion
2222
+ //#region src/client/cards.ts
2223
+ /** The bar fill and figure tone for a usage percentage: 70 warn / 90 danger. */
2224
+ function usageTone(pct) {
2225
+ if (pct >= 90) return {
2226
+ fill: S.barFillError,
2227
+ color: "var(--dsw-alias-state-error-primary)"
2228
+ };
2229
+ if (pct >= 70) return {
2230
+ fill: S.barFillWarn,
2231
+ color: "var(--dsw-alias-state-warn-primary)"
2232
+ };
2233
+ return {
2234
+ fill: S.barFill,
2235
+ color: "var(--dsw-alias-label-secondary)"
2236
+ };
2237
+ }
2238
+ /**
2239
+ * The window's own name, from the key the Host emits.
2240
+ *
2241
+ * Falls back to the raw key rather than to a generic "quota": an unmapped key
2242
+ * means the Host grew a dimension this bundle has never heard of, and showing
2243
+ * `requestsMonthly` is more useful to whoever has to fix that than showing
2244
+ * four identical "Quota" labels.
2245
+ */
2246
+ function windowLabel(key, tt) {
2247
+ const name = String(key ?? "");
2248
+ return name === "" ? tt("quota.window") : tt(`quota.win.${name}`);
2249
+ }
2250
+ /** The window's period, phrased the way the platform phrases it. */
2251
+ function windowPeriod(hours, tt) {
2252
+ const value = Number(hours);
2253
+ if (!Number.isFinite(value) || value <= 0) return "";
2254
+ if (value === 168) return tt("quota.perWeek");
2255
+ if (value === 24) return tt("quota.perDay");
2256
+ return format(tt("quota.perHours"), { hours: value });
2257
+ }
2258
+ /**
2259
+ * The unit a dimension is counted in — `""` for video.
2260
+ *
2261
+ * Video is deliberately unitless. The platform's limit field is
2262
+ * `video_daily_limit` while its usage field is `total_video_seconds`, and it
2263
+ * never says which one the limit is expressed in, so printing "500 秒" would
2264
+ * assert something nobody verified.
2265
+ */
2266
+ function unitOf(unit, tt) {
2267
+ const name = String(unit ?? "");
2268
+ if (name === "requests") return tt("quota.unit.requests");
2269
+ if (name === "images") return tt("quota.unit.images");
2270
+ return "";
2271
+ }
2272
+ /** `monthly` / `yearly` as the panel phrases them. */
2273
+ function cycleLabel(cycle, tt) {
2274
+ const name = String(cycle ?? "");
2275
+ if (name === "yearly" || name === "annual") return tt("quota.cycle.yearly");
2276
+ if (name === "monthly") return tt("quota.cycle.monthly");
2277
+ return name;
2278
+ }
2279
+ /**
2280
+ * One quota window as a compact sub-card.
2281
+ *
2282
+ * The LIMIT is the headline here, not a percentage — and that inversion is the
2283
+ * whole point of the Agnes rewrite. The SenseNova card led with "remaining %"
2284
+ * because the platform reported a live balance per pool. Agnes reports none:
2285
+ * it caps four dimensions over sliding windows and its console only publishes
2286
+ * cumulative usage, so `limit - total` would subtract two different periods and
2287
+ * print a number nobody can defend. The limit is a fact the platform states;
2288
+ * a "remaining" figure would be an invention.
2289
+ *
2290
+ * When the platform DOES state a window's consumption (a subscription payload
2291
+ * that carries `used`), the bar and the used caption appear below the limit —
2292
+ * that is the one case where the panel has a real fraction to draw.
2293
+ *
2294
+ * A window that is not an object at all (a row the Host flagged as
2295
+ * shape-drifted, or a field simply absent) renders NOTHING instead of throwing:
2296
+ * one malformed row must not blank the whole panel — the shape warning above
2297
+ * already says what is wrong.
2298
+ */
2299
+ function QuotaWindowCard({ label, window, tt }) {
2300
+ if (window === null || typeof window !== "object") return null;
2301
+ const source = window;
2302
+ const limit = Number(source.limit) || 0;
2303
+ const period = windowPeriod(source.windowHours, tt);
2304
+ const unit = unitOf(source.unit, tt);
2305
+ const used = typeof source.used === "number" && Number.isFinite(source.used) ? source.used : null;
2306
+ const pct = used !== null && limit > 0 ? Math.min(100, used / limit * 100) : null;
2307
+ const tone = usageTone(pct ?? 0);
2308
+ return h("div", { style: S.quota }, h("div", { style: S.quotaTop }, h("span", { style: S.quotaLabel }, label), period === "" ? null : h("span", { style: S.quotaReset }, period)), h("div", { style: S.quotaRemaining }, limit > 0 ? `${count(limit)}${unit === "" ? "" : ` ${unit}`}` : "—"), pct === null ? null : h("div", {
2309
+ style: S.bar,
2310
+ role: "progressbar",
2311
+ "aria-label": `${label} ${tt("quota.used")} ${pct.toFixed(1)}%`,
2312
+ "aria-valuenow": pct.toFixed(1),
2313
+ "aria-valuemin": 0,
2314
+ "aria-valuemax": 100
2315
+ }, h("div", { style: {
2316
+ ...tone.fill,
2317
+ width: `${pct}%`
2318
+ } })), pct === null ? null : h("span", { style: S.quotaUsed }, `${tt("quota.used")} ${count(used)} / ${count(limit)}`));
2319
+ }
2320
+ /**
2321
+ * The current plan: its identity, the four windows it caps, and — folded away
2322
+ * — what the other tiers would allow.
2323
+ *
2324
+ * The catalogue rides along because it is the ONE quota source that answers an
2325
+ * anonymous request, so it is what the card can still show when no account is
2326
+ * stored. Folding it into `<details>` keeps the open card about the reader's
2327
+ * own plan; expanding it answers "is upgrading worth it" with the platform's
2328
+ * own numbers rather than a link.
2329
+ */
2330
+ function PlanCard({ quota, tt }) {
2331
+ const plan = quota?.plan ?? null;
2332
+ const windows = Array.isArray(quota?.windows) ? quota.windows : [];
2333
+ const catalogue = Array.isArray(quota?.plans) ? quota.plans : [];
2334
+ if (plan === null && windows.length === 0 && catalogue.length === 0) return h("div", { style: S.card }, h("div", { style: S.empty }, tt("quota.none")));
2335
+ return h("div", { style: S.card }, plan === null ? h("div", { style: S.empty }, tt("quota.planUnknown")) : h("div", { style: S.cardHead }, h("span", { style: S.poolName }, plan.displayName || plan.name || tt("quota.planUnknown")), plan.billingCycle ? h("span", { style: S.chip }, cycleLabel(plan.billingCycle, tt)) : null, plan.priceMinor > 0 ? h("span", { style: S.grantChip }, `${money(plan.priceMinor, plan.currency)}${plan.billingCycle === "yearly" ? tt("quota.perYear") : tt("quota.perMonth")}`) : null, h("span", { style: S.spacer }), quota?.expiresAt ? h("span", { style: S.quotaReset }, format(tt("quota.expires"), { time: clockLong(quota.expiresAt) })) : null), plan?.usageLimitText ? h("div", { style: {
2336
+ ...S.muted,
2337
+ fontSize: 12,
2338
+ marginTop: 8
2339
+ } }, plan.usageLimitText) : null, windows.length > 0 ? h("div", { style: S.quotas }, windows.map((window) => h(QuotaWindowCard, {
2340
+ key: String(window?.key ?? ""),
2341
+ label: windowLabel(window?.key, tt),
2342
+ window,
2343
+ tt
2344
+ }))) : null, catalogue.length > 0 ? h("details", { style: S.details }, h("summary", { style: S.detailsSummary }, format(tt("quota.catalogue"), { count: catalogue.length })), h("div", { style: S.detailsBody }, catalogue.map((entry) => {
2345
+ const limits = entry.limits ?? {};
2346
+ const parts = [
2347
+ limits.requests5h > 0 ? `${count(limits.requests5h)}${tt("quota.unit.requests")} / ${limits.requestsWindowH || 5}h` : "",
2348
+ limits.requestsWeekly > 0 ? `${count(limits.requestsWeekly)}${tt("quota.unit.requests")} / ${tt("quota.perWeek")}` : "",
2349
+ limits.imagesDaily > 0 ? `${count(limits.imagesDaily)}${tt("quota.unit.images")} / ${tt("quota.perDay")}` : ""
2350
+ ].filter((part) => part !== "");
2351
+ return h("div", {
2352
+ key: entry.uuid || String(entry.planId),
2353
+ style: S.catalogueRow
2354
+ }, h("span", { style: S.catalogueName }, entry.displayName || entry.name || ""), h("span", { style: S.muted }, cycleLabel(entry.billingCycle, tt)), entry.priceMinor > 0 ? h("span", { style: S.muted }, money(entry.priceMinor, entry.currency)) : null, h("span", { style: S.catalogueLimits }, parts.join(" · ")));
2355
+ }))) : null);
2356
+ }
2357
+ /**
2358
+ * The consumption figures for one period, as a row of metric cells.
2359
+ *
2360
+ * `label` names the period ("account total" vs "last N days") because the two
2361
+ * blocks use the SAME figures with different meanings, and an unlabelled row
2362
+ * would let the reader take a lifetime total for a window total. `activeDays`
2363
+ * is only present on the account row — the series has no such field — and an
2364
+ * absent cell is simply not drawn rather than shown as 0.
2365
+ */
2366
+ function UsageTotals({ totals, label, tt }) {
2367
+ const source = totals ?? null;
2368
+ if (source === null) return h("div", { style: S.empty }, format(tt("quota.usageMissing"), { label }));
2369
+ const cells = [
2370
+ {
2371
+ key: "requests",
2372
+ label: tt("quota.total.requests"),
2373
+ value: source.totalRequests
2374
+ },
2375
+ {
2376
+ key: "tokens",
2377
+ label: tt("quota.total.tokens"),
2378
+ value: source.totalTokens
2379
+ },
2380
+ {
2381
+ key: "images",
2382
+ label: tt("quota.total.images"),
2383
+ value: source.totalImages
2384
+ },
2385
+ {
2386
+ key: "video",
2387
+ label: tt("quota.total.video"),
2388
+ value: source.totalVideoSeconds
2389
+ },
2390
+ ...typeof source.activeDays === "number" ? [{
2391
+ key: "days",
2392
+ label: tt("quota.total.activeDays"),
2393
+ value: source.activeDays
2394
+ }] : []
2395
+ ];
2396
+ return h("div", null, h("div", { style: {
2397
+ ...S.muted,
2398
+ fontSize: 12,
2399
+ marginBottom: 8
2400
+ } }, label), h("div", { style: S.metricGrid }, cells.map((cell) => h("div", {
2401
+ key: cell.key,
2402
+ style: S.metric
2403
+ }, h("span", { style: S.metricLabel }, cell.label), h("span", { style: S.metricValue }, count(cell.value))))));
2404
+ }
2405
+ /**
2406
+ * Per-bucket consumption as a mini bar chart.
2407
+ *
2408
+ * The x axis is the platform's OWN bucket label, not a date this client
2409
+ * re-derives: the series endpoint takes dates and answers with whatever
2410
+ * granularity it chose, so re-formatting `bucket` would be the panel asserting
2411
+ * a granularity the platform never promised. Only the first and last labels are
2412
+ * printed — 30 daily bars cannot each carry a readable date, and every bar
2413
+ * carries its own `title` for the one the reader hovers.
2414
+ *
2415
+ * The bars are scaled to the LARGEST bucket, so the tallest always fills the
2416
+ * track; that answers "when was the heavy day", but the eye misreads a full
2417
+ * track as "at the limit", so the legend names the convention.
2418
+ */
2419
+ function UsageChart({ usage, tt }) {
2420
+ const buckets = Array.isArray(usage?.buckets) ? usage.buckets : [];
2421
+ if (buckets.length === 0) return h("div", { style: S.card }, h("div", { style: S.empty }, tt("usage.none")));
2422
+ const values = buckets.map((bucket) => Math.max(0, Number(bucket?.requestCount) || 0));
2423
+ const max = Math.max(0, ...values);
2424
+ const first = String(buckets[0]?.bucket ?? "");
2425
+ const last = String(buckets[buckets.length - 1]?.bucket ?? "");
2426
+ return h("div", { style: S.card }, h("div", { style: S.trendHead }, h("span", { style: S.trendHeadLabel }, tt("usage.requests")), h("span", { style: {
2427
+ ...S.trendHeadLabel,
2428
+ textAlign: "right"
2429
+ } }, tt("usage.perBucket"))), h("div", { style: S.usageBars }, buckets.map((bucket, index) => {
2430
+ const value = values[index];
2431
+ const pct = max > 0 ? value / max * 100 : 0;
2432
+ const label = String(bucket?.bucket ?? "");
2433
+ return h("div", {
2434
+ key: `${label}-${index}`,
2435
+ style: S.usageBar,
2436
+ title: `${label}: ${count(value)} ${tt("quota.unit.requests")}`
2437
+ }, h("div", {
2438
+ style: {
2439
+ ...S.usageBarFill,
2440
+ height: `${pct}%`
2441
+ },
2442
+ role: "progressbar",
2443
+ "aria-label": `${label} ${count(value)}`,
2444
+ "aria-valuenow": Math.round(pct),
2445
+ "aria-valuemin": 0,
2446
+ "aria-valuemax": 100
2447
+ }));
2448
+ })), h("div", { style: S.usageAxis }, h("span", null, first), h("span", null, last)), h("div", { style: S.trendLegend }, tt("usage.legend")));
2449
+ }
2450
+ /**
2451
+ * One content section as a workbuddy-style collapsible card: a full-width
2452
+ * header button (title + rotating chevron) over a bordered card body.
2453
+ * Auto-expanded by default in `PanelPage`; the reader can tuck a section
2454
+ * away to focus on the other. Hook-free on purpose — `open` and `onToggle`
2455
+ * arrive as props, so the render tests exercise the toggle without faking
2456
+ * React state (children travel as a regular `children` prop, as in React).
2457
+ */
2458
+ function SectionCard({ title, open, onToggle, children, tt }) {
2459
+ return h("div", { style: S.sectionCard }, h("button", {
2460
+ type: "button",
2461
+ style: S.sectionHead,
2462
+ "aria-expanded": open,
2463
+ "aria-label": `${tt(open ? "section.collapse" : "section.expand")}: ${title}`,
2464
+ onClick: onToggle
2465
+ }, h("span", { style: S.sectionHeadTitle }, title), h("svg", {
2466
+ viewBox: "0 0 16 16",
2467
+ width: 14,
2468
+ height: 14,
2469
+ fill: "none",
2470
+ stroke: "currentColor",
2471
+ strokeWidth: "1.5",
2472
+ strokeLinecap: "round",
2473
+ strokeLinejoin: "round",
2474
+ "aria-hidden": "true",
2475
+ style: open ? {
2476
+ ...S.chevron,
2477
+ ...S.chevronOpen
2478
+ } : S.chevron
2479
+ }, h("path", { d: "M3 6l5 5 5-5" }))), h("div", {
2480
+ style: S.sectionBody,
2481
+ hidden: !open
2482
+ }, open ? children : null));
2483
+ }
2484
+ var init_cards = __esmMin((() => {
2485
+ init_format();
2486
+ init_runtime();
2487
+ init_styles();
2488
+ }));
2489
+
2490
+ //#endregion
2491
+ //#region src/client/qr.ts
2492
+ function gfMul(a, b) {
2493
+ if (a === 0 || b === 0) return 0;
2494
+ return GF_EXP[GF_LOG[a] + GF_LOG[b]];
2495
+ }
2496
+ function polyMul(a, b) {
2497
+ const result = new Array(a.length + b.length - 1).fill(0);
2498
+ for (let i = 0; i < a.length; i++) for (let j = 0; j < b.length; j++) result[i + j] ^= gfMul(a[i], b[j]);
2499
+ return result;
2500
+ }
2501
+ function rsGeneratorPoly(degree) {
2502
+ let poly = [1];
2503
+ for (let i = 0; i < degree; i++) poly = polyMul(poly, [1, GF_EXP[i]]);
2504
+ return poly;
2505
+ }
2506
+ /** The Reed–Solomon remainder for one data stream. */
2507
+ function rsEncode(data, ecCount) {
2508
+ const generator = rsGeneratorPoly(ecCount);
2509
+ const buffer = data.concat(new Array(ecCount).fill(0));
2510
+ for (let i = 0; i < data.length; i++) {
2511
+ const coefficient = buffer[i];
2512
+ if (coefficient === 0) continue;
2513
+ for (let j = 0; j < generator.length; j++) buffer[i + j] ^= gfMul(generator[j], coefficient);
2514
+ }
2515
+ return buffer.slice(data.length);
2516
+ }
2517
+ /**
2518
+ * The alignment-pattern centre coordinates per version.
2519
+ * v1 has none; v2–6 use `[6, size-7]`; v7–v10 follow the ISO table.
2520
+ */
2521
+ function alignmentCentres(version) {
2522
+ if (version <= 1) return [];
2523
+ return [
2524
+ null,
2525
+ null,
2526
+ [6, 18],
2527
+ [6, 22],
2528
+ [6, 26],
2529
+ [6, 30],
2530
+ [6, 34],
2531
+ [
2532
+ 6,
2533
+ 22,
2534
+ 38
2535
+ ],
2536
+ [
2537
+ 6,
2538
+ 24,
2539
+ 42
2540
+ ],
2541
+ [
2542
+ 6,
2543
+ 26,
2544
+ 46
2545
+ ],
2546
+ [
2547
+ 6,
2548
+ 28,
2549
+ 50
2550
+ ]
2551
+ ][version] ?? [];
2552
+ }
2553
+ /** Pick the smallest version (1–10) that holds `byteLength` bytes, or -1. */
2554
+ function pickVersion(byteLength) {
2555
+ for (let version = 1; version <= 10; version++) {
2556
+ const capacityBits = DATA_CODEWORDS[version] * 8;
2557
+ if (4 + (version <= 9 ? 8 : 16) + byteLength * 8 <= capacityBits) return version;
2558
+ }
2559
+ return -1;
2560
+ }
2561
+ /** Encode the payload bytes as the bit stream the codeword interleave consumes. */
2562
+ function buildCodewords(bytes, version) {
2563
+ const capacityBits = DATA_CODEWORDS[version] * 8;
2564
+ const bits = [];
2565
+ const pushBits = (value, length) => {
2566
+ for (let i = length - 1; i >= 0; i--) bits.push(value >> i & 1);
2567
+ };
2568
+ pushBits(4, 4);
2569
+ pushBits(bytes.length, version <= 9 ? 8 : 16);
2570
+ for (const byte of bytes) pushBits(byte, 8);
2571
+ pushBits(0, Math.min(4, capacityBits - bits.length));
2572
+ while (bits.length % 8 !== 0) bits.push(0);
2573
+ for (let i = 0; bits.length < capacityBits; i++) pushBits(i % 2 === 0 ? 236 : 17, 8);
2574
+ const dataCodewords = [];
2575
+ for (let i = 0; i < bits.length; i += 8) {
2576
+ let byte = 0;
2577
+ for (let j = 0; j < 8; j++) byte = byte << 1 | bits[i + j];
2578
+ dataCodewords.push(byte);
2579
+ }
2580
+ const blocks = EC_BLOCKS_M[version - 1];
2581
+ const dataBlocks = [];
2582
+ const ecBlocks = [];
2583
+ let offset = 0;
2584
+ for (const [count, dataPerBlock] of blocks.groups) for (let b = 0; b < count; b++) {
2585
+ const block = dataCodewords.slice(offset, offset + dataPerBlock);
2586
+ offset += dataPerBlock;
2587
+ dataBlocks.push(block);
2588
+ ecBlocks.push(rsEncode(block, blocks.ecPerBlock));
2589
+ }
2590
+ const result = [];
2591
+ const maxDataLen = Math.max(...dataBlocks.map((block) => block.length));
2592
+ for (let i = 0; i < maxDataLen; i++) for (const block of dataBlocks) if (i < block.length) result.push(block[i]);
2593
+ for (let i = 0; i < blocks.ecPerBlock; i++) for (const block of ecBlocks) if (i < block.length) result.push(block[i]);
2594
+ return result;
2595
+ }
2596
+ /**
2597
+ * The QR module grid before masking: the finder / alignment / timing / dark
2598
+ * module / format-placeholder cells are placed, and the codewords fill the
2599
+ * remaining cells in the ISO zig-zag. `isFunction` records which cells a mask
2600
+ * (or the final format write) must never touch.
2601
+ */
2602
+ function buildMatrix(size, codewords, version) {
2603
+ const modules = [];
2604
+ const isFunction = [];
2605
+ for (let row = 0; row < size; row++) {
2606
+ modules.push(new Array(size).fill(false));
2607
+ isFunction.push(new Array(size).fill(false));
2608
+ }
2609
+ const set = (row, col, dark) => {
2610
+ modules[row][col] = dark;
2611
+ isFunction[row][col] = true;
2612
+ };
2613
+ for (let i = 0; i < size; i++) {
2614
+ set(6, i, i % 2 === 0);
2615
+ set(i, 6, i % 2 === 0);
2616
+ }
2617
+ const finder = (cx, cy) => {
2618
+ for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
2619
+ const xx = cx + dx;
2620
+ const yy = cy + dy;
2621
+ if (xx < 0 || xx >= size || yy < 0 || yy >= size) continue;
2622
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
2623
+ set(yy, xx, dist !== 2 && dist !== 4);
2624
+ }
2625
+ };
2626
+ finder(3, 3);
2627
+ finder(size - 4, 3);
2628
+ finder(3, size - 4);
2629
+ const centres = alignmentCentres(version);
2630
+ for (const row of centres) for (const col of centres) {
2631
+ if (row === 6 && col === 6 || row === 6 && col === size - 7 || row === size - 7 && col === 6) continue;
2632
+ for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(row + dy, col + dx, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
2633
+ }
2634
+ set(size - 8, 8, true);
2635
+ const reserveFormat = (row, col) => {
2636
+ if (row >= 0 && row < size && col >= 0 && col < size) {
2637
+ modules[row][col] = false;
2638
+ isFunction[row][col] = true;
2639
+ }
2640
+ };
2641
+ for (let i = 0; i <= 8; i++) {
2642
+ reserveFormat(i, 8);
2643
+ reserveFormat(8, i);
2644
+ }
2645
+ for (let i = 0; i < 8; i++) {
2646
+ reserveFormat(size - 1 - i, 8);
2647
+ reserveFormat(8, size - 1 - i);
2648
+ }
2649
+ if (version >= 7) for (let i = 0; i < 18; i++) {
2650
+ const a = size - 11 + i % 3;
2651
+ const b = Math.floor(i / 3);
2652
+ reserveFormat(b, a);
2653
+ reserveFormat(a, b);
2654
+ }
2655
+ let bitIndex = 0;
2656
+ for (let right = size - 1; right >= 1; right -= 2) {
2657
+ if (right === 6) right = 5;
2658
+ const upward = (right + 1 & 2) === 0;
2659
+ for (let vert = 0; vert < size; vert++) for (let j = 0; j < 2; j++) {
2660
+ const col = right - j;
2661
+ const row = upward ? size - 1 - vert : vert;
2662
+ if (isFunction[row][col]) continue;
2663
+ if (bitIndex < codewords.length * 8) {
2664
+ const byte = codewords[bitIndex >> 3];
2665
+ modules[row][col] = (byte >> 7 - (bitIndex & 7) & 1) === 1;
2666
+ bitIndex++;
2667
+ }
2668
+ }
2669
+ }
2670
+ return {
2671
+ modules,
2672
+ isFunction
2673
+ };
2674
+ }
2675
+ /** The ISO mask functions; mask 7 is the default case. */
2676
+ function maskInvert(row, col, mask) {
2677
+ switch (mask) {
2678
+ case 0: return (row + col) % 2 === 0;
2679
+ case 1: return row % 2 === 0;
2680
+ case 2: return col % 3 === 0;
2681
+ case 3: return (row + col) % 3 === 0;
2682
+ case 4: return (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0;
2683
+ case 5: return row * col % 2 + row * col % 3 === 0;
2684
+ case 6: return (row * col % 2 + row * col % 3) % 2 === 0;
2685
+ default: return ((row + col) % 2 + row * col % 3) % 2 === 0;
2686
+ }
2687
+ }
2688
+ /** The 15-bit format information for one mask at EC level M. */
2689
+ function formatBits(mask) {
2690
+ const data = 0 | mask & 7;
2691
+ let rem = data;
2692
+ for (let i = 0; i < 10; i++) rem = rem << 1 ^ (rem >>> 9) * 1335;
2693
+ return (data << 10 | rem) ^ 21522;
2694
+ }
2695
+ /**
2696
+ * Write the format bits, in both copies, marking them function cells so the
2697
+ * mask never disturbs them. The bit indices are the ISO ordering:
2698
+ *
2699
+ * copy 1: bit i → (col 8, row i) for i ≤ 5; bit 6 → (8,7); bit 7 → (8,8);
2700
+ * bit 8 → (7,8); bit i → (row 14-i, col 8) for i = 9..14.
2701
+ * copy 2: bit i → (row size-1-i, col 8) for i ≤ 7; bit i → (row 8,
2702
+ * col size-7+i-8) for i = 8..14.
2703
+ */
2704
+ function writeFormat(modules, isFunction, mask) {
2705
+ const size = modules.length;
2706
+ const bits = formatBits(mask);
2707
+ const bit = (i) => (bits >>> i & 1) === 1;
2708
+ const set = (row, col, dark) => {
2709
+ modules[row][col] = dark;
2710
+ isFunction[row][col] = true;
2711
+ };
2712
+ for (let i = 0; i <= 5; i++) set(i, 8, bit(i));
2713
+ set(7, 8, bit(6));
2714
+ set(8, 8, bit(7));
2715
+ set(8, 7, bit(8));
2716
+ for (let i = 9; i < 15; i++) set(8, 14 - i, bit(i));
2717
+ for (let i = 0; i < 8; i++) set(8, size - 1 - i, bit(i));
2718
+ for (let i = 8; i < 15; i++) set(size - 15 + i, 8, bit(i));
2719
+ set(size - 8, 8, true);
2720
+ }
2721
+ /** The 18-bit version block (v7+), placed above the bottom-left finder. */
2722
+ function writeVersion(modules, isFunction, version) {
2723
+ const size = modules.length;
2724
+ let rem = version;
2725
+ for (let i = 0; i < 12; i++) rem = rem << 1 ^ (rem >>> 11) * 7973;
2726
+ const bits = version << 12 | rem;
2727
+ const set = (row, col, dark) => {
2728
+ modules[row][col] = dark;
2729
+ isFunction[row][col] = true;
2730
+ };
2731
+ for (let i = 0; i < 18; i++) {
2732
+ const dark = (bits >> i & 1) === 1;
2733
+ const a = size - 11 + i % 3;
2734
+ const b = Math.floor(i / 3);
2735
+ set(b, a, dark);
2736
+ set(a, b, dark);
2737
+ }
2738
+ }
2739
+ /**
2740
+ * The four ISO penalty rules (lower = better mask): N1 same-colour runs, N2
2741
+ * 2×2 blocks, N3 finder-like patterns, N4 the dark-fraction balance.
2742
+ */
2743
+ function penaltyScore(modules) {
2744
+ const size = modules.length;
2745
+ let score = 0;
2746
+ const linePenalty = (line) => {
2747
+ let result = 0;
2748
+ let runLength = 1;
2749
+ for (let i = 1; i < line.length; i++) if (line[i] === line[i - 1]) runLength++;
2750
+ else {
2751
+ if (runLength >= 5) result += 3 + (runLength - 5);
2752
+ runLength = 1;
2753
+ }
2754
+ if (runLength >= 5) result += 3 + (runLength - 5);
2755
+ const PATTERN_A = [
2756
+ true,
2757
+ false,
2758
+ true,
2759
+ true,
2760
+ true,
2761
+ false,
2762
+ true,
2763
+ false,
2764
+ false,
2765
+ false,
2766
+ false
2767
+ ];
2768
+ const PATTERN_B = [
2769
+ false,
2770
+ false,
2771
+ false,
2772
+ false,
2773
+ true,
2774
+ false,
2775
+ true,
2776
+ true,
2777
+ true,
2778
+ false,
2779
+ true
2780
+ ];
2781
+ for (let i = 0; i + 11 <= line.length; i++) {
2782
+ let matchA = true;
2783
+ let matchB = true;
2784
+ for (let j = 0; j < 11; j++) {
2785
+ if (line[i + j] !== PATTERN_A[j]) matchA = false;
2786
+ if (line[i + j] !== PATTERN_B[j]) matchB = false;
2787
+ if (!matchA && !matchB) break;
2788
+ }
2789
+ if (matchA) result += 40;
2790
+ if (matchB) result += 40;
2791
+ }
2792
+ return result;
2793
+ };
2794
+ for (let row = 0; row < size; row++) score += linePenalty(modules[row]);
2795
+ for (let col = 0; col < size; col++) {
2796
+ const column = [];
2797
+ for (let row = 0; row < size; row++) column.push(modules[row][col]);
2798
+ score += linePenalty(column);
2799
+ }
2800
+ for (let row = 0; row < size - 1; row++) for (let col = 0; col < size - 1; col++) {
2801
+ const value = modules[row][col];
2802
+ if (value === modules[row][col + 1] && value === modules[row + 1][col] && value === modules[row + 1][col + 1]) score += 3;
2803
+ }
2804
+ let dark = 0;
2805
+ for (const row of modules) for (const cell of row) if (cell) dark++;
2806
+ const total = size * size;
2807
+ const k = Math.floor(Math.abs(dark * 20 - total * 10) / total + 1) - 1;
2808
+ score += Math.max(0, k) * 10;
2809
+ return score;
2810
+ }
2811
+ /**
2812
+ * Build the matrix for one payload: encode, place, mask (the lowest-penalty
2813
+ * of the eight), write the format bits, and the version block for v7+.
2814
+ * @throws {Error} when the payload does not fit versions 1–10 at level M.
2815
+ */
2816
+ function buildQrMatrix(text) {
2817
+ const bytes = Array.from(new TextEncoder().encode(text));
2818
+ const version = pickVersion(bytes.length);
2819
+ if (version < 1) throw new Error(`qr: payload of ${bytes.length} bytes exceeds the v1–10/M capacity`);
2820
+ const size = 17 + version * 4;
2821
+ const { modules, isFunction } = buildMatrix(size, buildCodewords(bytes, version), version);
2822
+ let bestMask = 0;
2823
+ let bestPenalty = Infinity;
2824
+ for (let mask = 0; mask < 8; mask++) {
2825
+ for (let row = 0; row < size; row++) for (let col = 0; col < size; col++) {
2826
+ if (isFunction[row][col]) continue;
2827
+ if (maskInvert(row, col, mask)) modules[row][col] = !modules[row][col];
2828
+ }
2829
+ writeFormat(modules, isFunction, mask);
2830
+ if (version >= 7) writeVersion(modules, isFunction, version);
2831
+ const score = penaltyScore(modules);
2832
+ if (score < bestPenalty) {
2833
+ bestPenalty = score;
2834
+ bestMask = mask;
2835
+ }
2836
+ for (let row = 0; row < size; row++) for (let col = 0; col < size; col++) {
2837
+ if (isFunction[row][col]) continue;
2838
+ if (maskInvert(row, col, mask)) modules[row][col] = !modules[row][col];
2839
+ }
2840
+ }
2841
+ for (let row = 0; row < size; row++) for (let col = 0; col < size; col++) {
2842
+ if (isFunction[row][col]) continue;
2843
+ if (maskInvert(row, col, bestMask)) modules[row][col] = !modules[row][col];
2844
+ }
2845
+ writeFormat(modules, isFunction, bestMask);
2846
+ if (version >= 7) writeVersion(modules, isFunction, version);
2847
+ return {
2848
+ size,
2849
+ modules
2850
+ };
2851
+ }
2852
+ /**
2853
+ * Render a matrix as a compact SVG data-URL the panel can drop straight into
2854
+ * an `<img>` (no further client-side encoding logic is needed).
2855
+ * @param {string} text - the payload.
2856
+ * @param {object} [options]
2857
+ * @param {number} [options.size] - rendered pixel size (default 200).
2858
+ * @returns {string} `data:image/svg+xml;utf8,` + the URL-encoded SVG.
2859
+ */
2860
+ function qrDataUrl(text, options = {}) {
2861
+ const { size, modules } = buildQrMatrix(text);
2862
+ const margin = 4;
2863
+ const dim = size + 8;
2864
+ const segments = [];
2865
+ for (let row = 0; row < size; row++) for (let col = 0; col < size; col++) if (modules[row][col]) segments.push(`M${col + margin} ${row + margin}h1v1h-1z`);
2866
+ const px = options.size ?? 200;
2867
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${px}" viewBox="0 0 ${dim} ${dim}" shape-rendering="crispEdges"><rect width="${dim}" height="${dim}" fill="#fff"/><path d="${segments.join("")}" fill="#000"/></svg>`;
2868
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
2869
+ }
2870
+ var DATA_CODEWORDS, EC_BLOCKS_M, GF_EXP, GF_LOG;
2871
+ var init_qr = __esmMin((() => {
2872
+ DATA_CODEWORDS = [
2873
+ 0,
2874
+ 16,
2875
+ 28,
2876
+ 44,
2877
+ 64,
2878
+ 86,
2879
+ 108,
2880
+ 124,
2881
+ 154,
2882
+ 182,
2883
+ 216
2884
+ ];
2885
+ EC_BLOCKS_M = [
2886
+ {
2887
+ ecPerBlock: 10,
2888
+ groups: [[1, 16]]
2889
+ },
2890
+ {
2891
+ ecPerBlock: 16,
2892
+ groups: [[1, 28]]
2893
+ },
2894
+ {
2895
+ ecPerBlock: 26,
2896
+ groups: [[1, 44]]
2897
+ },
2898
+ {
2899
+ ecPerBlock: 18,
2900
+ groups: [[2, 32]]
2901
+ },
2902
+ {
2903
+ ecPerBlock: 24,
2904
+ groups: [[2, 43]]
2905
+ },
2906
+ {
2907
+ ecPerBlock: 16,
2908
+ groups: [[4, 27]]
2909
+ },
2910
+ {
2911
+ ecPerBlock: 18,
2912
+ groups: [[4, 31]]
2913
+ },
2914
+ {
2915
+ ecPerBlock: 22,
2916
+ groups: [[2, 38], [2, 39]]
2917
+ },
2918
+ {
2919
+ ecPerBlock: 22,
2920
+ groups: [[3, 36], [2, 37]]
2921
+ },
2922
+ {
2923
+ ecPerBlock: 26,
2924
+ groups: [[4, 43], [1, 44]]
2925
+ }
2926
+ ];
2927
+ GF_EXP = /* @__PURE__ */ new Uint8Array(512);
2928
+ GF_LOG = /* @__PURE__ */ new Uint8Array(256);
2929
+ {
2930
+ let x = 1;
2931
+ for (let i = 0; i < 255; i++) {
2932
+ GF_EXP[i] = x;
2933
+ GF_LOG[x] = i;
2934
+ x <<= 1;
2935
+ if ((x & 256) !== 0) x ^= 285;
2936
+ }
2937
+ for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];
2938
+ }
2939
+ }));
2940
+
2941
+ //#endregion
2942
+ //#region src/client/raccoon-tab.ts
2943
+ /**
2944
+ * The QR image the login code encodes. The payload is the gateway's own
2945
+ * public login page URL (~144 bytes), which fits the v1–10/M capacity the
2946
+ * local encoder supports; `buildQrMatrix` throwing is the out-of-range
2947
+ * signal, and the tab then falls back to the plain URL text.
2948
+ */
2949
+ function qrImageOf(scanUrl) {
2950
+ if (typeof scanUrl !== "string" || scanUrl === "") return null;
2951
+ try {
2952
+ return h("img", {
2953
+ src: qrDataUrl(scanUrl, { size: 208 }),
2954
+ alt: "WeChat QR",
2955
+ width: 208,
2956
+ height: 208,
2957
+ style: {
2958
+ display: "block",
2959
+ margin: "8px 0",
2960
+ borderRadius: 4
2961
+ }
2962
+ });
2963
+ } catch {
2964
+ return h("code", { style: {
2965
+ ...S.muted,
2966
+ fontSize: 12,
2967
+ wordBreak: "break-all"
2968
+ } }, scanUrl);
2969
+ }
2970
+ }
2971
+ /**
2972
+ * The Raccoon tab body.
2973
+ * @param {object} props
2974
+ * @param {Tt} props.tt - the dictionary.
2975
+ * @returns {unknown} the tab's card tree.
2976
+ */
2977
+ function RaccoonTab({ tt }) {
2978
+ const [state, setState] = useState(null);
2979
+ const [loading, setLoading] = useState(true);
2980
+ const [error, setError] = useState(null);
2981
+ const [loginBusy, setLoginBusy] = useState(false);
2982
+ const [loginNote, setLoginNote] = useState(null);
2983
+ const alive = useRef(true);
2984
+ const pollMs = useRef(RACCOON_POLL_MS);
2985
+ const load = useCallback(async () => {
2986
+ const generation = pollMs.current;
2987
+ try {
2988
+ const response = await fetch(RACCOON_PATH, {
2989
+ headers: { accept: "application/json" },
2990
+ cache: "no-store"
2991
+ });
2992
+ if (!response.ok || !alive.current || generation !== pollMs.current) return;
2993
+ const body = await response.json().catch(() => null);
2994
+ if (!alive.current || generation !== pollMs.current) return;
2995
+ if (body === null || body.ok === false) {
2996
+ setError(typeof body?.error === "string" && body.error !== "" ? body.error : "no answer");
2997
+ return;
2998
+ }
2999
+ setState(body);
3000
+ setError(null);
3001
+ } catch {
3002
+ if (alive.current && generation === pollMs.current) setError("unable to reach the Host");
3003
+ } finally {
3004
+ if (alive.current) setLoading(false);
3005
+ }
3006
+ }, []);
3007
+ useEffect(() => {
3008
+ alive.current = true;
3009
+ let timer = null;
3010
+ const run = () => {
3011
+ if (alive.current) load();
3012
+ };
3013
+ run();
3014
+ timer = setInterval(run, pollMs.current);
3015
+ return () => {
3016
+ alive.current = false;
3017
+ if (timer !== null) clearInterval(timer);
3018
+ };
3019
+ }, [load]);
3020
+ const toggle = useCallback(async (enabled) => {
3021
+ setLoginNote(null);
3022
+ try {
3023
+ const body = await postJsonOrThrow(RACCOON_PATH, {
3024
+ action: "switch",
3025
+ enabled
3026
+ });
3027
+ if (alive.current) setState((current) => current ? {
3028
+ ...current,
3029
+ enabled: body.enabled === true,
3030
+ providerRegistered: body.providerRegistered === true
3031
+ } : current);
3032
+ } catch (why) {
3033
+ if (alive.current) setLoginNote(format(tt("raccoon.switchError"), { error: why instanceof Error ? why.message : String(why) }));
3034
+ }
3035
+ }, [tt]);
3036
+ const startLogin = useCallback(async () => {
3037
+ setLoginBusy(true);
3038
+ setLoginNote(null);
3039
+ const quick = async () => {
3040
+ for (let i = 0; i < 150; i++) {
3041
+ await load();
3042
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
3043
+ }
3044
+ };
3045
+ quick();
3046
+ try {
3047
+ const body = await postJson(RACCOON_PATH, { action: "login" });
3048
+ if (alive.current) {
3049
+ if (body?.ok === true) setLoginNote(null);
3050
+ else setLoginNote(body?.error ?? tt("raccoon.error").replace("{error}", "login did not finish"));
3051
+ load();
3052
+ }
3053
+ } catch (why) {
3054
+ if (alive.current) setLoginNote(format(tt("raccoon.error"), { error: why instanceof Error ? why.message : String(why) }));
3055
+ } finally {
3056
+ if (alive.current) setLoginBusy(false);
3057
+ }
3058
+ }, [load, tt]);
3059
+ const logout = useCallback(async () => {
3060
+ setLoginNote(null);
3061
+ try {
3062
+ await postJsonOrThrow(RACCOON_PATH, { action: "logout" });
3063
+ if (alive.current) load();
3064
+ } catch (why) {
3065
+ if (alive.current) setLoginNote(format(tt("raccoon.error"), { error: why instanceof Error ? why.message : String(why) }));
3066
+ }
3067
+ }, [load, tt]);
3068
+ const enabled = state?.enabled === true;
3069
+ const loggedIn = state?.loggedIn === true;
3070
+ const models = Array.isArray(state?.models) ? state.models : [];
3071
+ return h("div", null, h("div", { style: {
3072
+ fontSize: 12,
3073
+ color: "var(--dsw-alias-label-secondary)",
3074
+ marginBottom: 12
3075
+ } }, tt("raccoon.desc")), h("label", { style: {
3076
+ display: "flex",
3077
+ gap: 8,
3078
+ alignItems: "center",
3079
+ margin: "0 0 12px",
3080
+ cursor: loginBusy ? "wait" : "pointer"
3081
+ } }, h("input", {
3082
+ type: "checkbox",
3083
+ checked: enabled,
3084
+ disabled: loginBusy,
3085
+ onChange: () => void toggle(!enabled)
3086
+ }), h("span", { style: {
3087
+ fontSize: 12,
3088
+ color: "var(--dsw-alias-label-secondary)"
3089
+ } }, tt("raccoon.switch"))), state !== null ? state.providerRegistered === true ? h("div", {
3090
+ style: {
3091
+ fontSize: 12,
3092
+ color: "var(--dsw-alias-label-secondary)"
3093
+ },
3094
+ role: "status"
3095
+ }, format(tt("raccoon.registered"), { count: count(models.length) })) : state.providerError !== void 0 && state.providerError !== "" ? h("div", {
3096
+ style: S.formError,
3097
+ role: "alert"
3098
+ }, state.providerError) : enabled && !loggedIn ? h("div", { style: {
3099
+ ...S.muted,
3100
+ fontSize: 12
3101
+ } }, tt("raccoon.awaitingLogin")) : h("div", { style: {
3102
+ ...S.muted,
3103
+ fontSize: 12
3104
+ } }, tt("raccoon.unregistered")) : null, h("div", { style: {
3105
+ ...S.card,
3106
+ marginTop: 4
3107
+ } }, loggedIn ? h("div", null, h("div", {
3108
+ style: { fontSize: 13 },
3109
+ role: "status"
3110
+ }, format(tt("raccoon.loggedIn"), { nick: String(state?.nickname ?? "") })), h("button", {
3111
+ type: "button",
3112
+ style: S.button,
3113
+ onClick: () => void logout()
3114
+ }, tt("raccoon.logout"))) : h("div", null, h("div", { style: { fontSize: 13 } }, tt("raccoon.notLogged")), state?.scanUrl !== void 0 && state?.scanUrl !== "" ? qrImageOf(state.scanUrl) : null, h("button", {
3115
+ type: "button",
3116
+ style: S.button,
3117
+ onClick: () => void startLogin(),
3118
+ disabled: loginBusy
3119
+ }, loginBusy ? tt("raccoon.loggingIn") : tt("raccoon.login")))), loginNote !== null ? h("div", {
3120
+ style: {
3121
+ ...S.formNote,
3122
+ fontSize: 12,
3123
+ marginTop: 8
3124
+ },
3125
+ role: "status"
3126
+ }, loginNote) : null, loggedIn ? h("div", { style: { marginTop: 12 } }, h("div", { style: {
3127
+ ...S.muted,
3128
+ fontSize: 12,
3129
+ marginBottom: 8
3130
+ } }, format(tt("raccoon.balance"), { balance: count(state?.balance ?? 0) })), models.length > 0 ? h(RaccoonRoster, {
3131
+ models,
3132
+ tt
3133
+ }) : null) : null);
3134
+ }
3135
+ /**
3136
+ * The model roster the Raccoon adapter offers, as a hook-free component.
3137
+ *
3138
+ * Split out of {@link RaccoonTab} for the same reason `ModelRoster` is its own
3139
+ * component: the tab's data is internal state, so the render suite can only
3140
+ * ever reach the logged-out frame — a roster inlined there is unassertable,
3141
+ * and the row-shape regression that broke the draw card would sail through
3142
+ * again. Both rosters now share `S.modelRow`'s contract and are pinned by the
3143
+ * same check.
3144
+ *
3145
+ * The row is the SAME two-line shape as the Token Plan roster's (head line
3146
+ * over an indented parameter line), because the data was already there: the
3147
+ * gateway's catalogue row carries `context_window` / `max_output_tokens`, the
3148
+ * Host has normalized them all along, and only this component was dropping
3149
+ * them — a name-only row reads as a stub beside the sibling list, not as a
3150
+ * deliberate minimalism. What it does NOT carry is the thinking ladder (see
3151
+ * the note in the row builder): that would be inventing a fact.
3152
+ * @param {object} props
3153
+ * @param {RaccoonModel[]} props.models - the rows the route reported.
3154
+ * @param {import("./runtime.ts").Tt} props.tt - the dictionary.
3155
+ * @returns {unknown} the roster list element.
3156
+ */
3157
+ function RaccoonRoster({ models, tt }) {
3158
+ const rows = Array.isArray(models) ? models : [];
3159
+ return h("div", { style: S.modelPanel }, h("div", { style: {
3160
+ ...S.muted,
3161
+ fontSize: 12,
3162
+ marginBottom: 6
3163
+ } }, format(tt("raccoon.models"), { count: count(rows.length) })), h("ul", {
3164
+ style: S.modelList,
3165
+ role: "list"
3166
+ }, rows.map((row) => {
3167
+ const id = String(row?.id ?? "");
3168
+ const label = String(row?.name ?? id);
3169
+ const rate = typeof row?.multiplier === "number" ? row.multiplier : null;
3170
+ const meta = [typeof row?.contextWindow === "number" && row.contextWindow > 0 ? format(tt("llm.contextBadge"), { ctx: tokenSize(row.contextWindow) }) : null, typeof row?.maxOutputLength === "number" && row.maxOutputLength > 0 ? format(tt("llm.metaOutput"), { out: tokenSize(row.maxOutputLength) }) : null].filter(Boolean).join(" · ");
3171
+ return h("li", {
3172
+ key: id,
3173
+ style: S.modelRow
3174
+ }, h("div", { style: S.modelRowHead }, h("span", {
3175
+ style: S.modelName,
3176
+ title: id
3177
+ }, label), rate !== null ? h("span", {
3178
+ style: S.modelRate,
3179
+ title: tt("raccoon.rateTitle")
3180
+ }, rate === 0 ? tt("raccoon.free") : `×${rate}`) : null, h("span", { style: S.spacer }), row.vision === true ? h("span", { style: S.modelBadge }, tt("llm.rosterVision")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
3181
+ })));
3182
+ }
3183
+ var RACCOON_POLL_MS;
3184
+ var init_raccoon_tab = __esmMin((() => {
3185
+ init_const();
3186
+ init_format();
3187
+ init_http();
3188
+ init_runtime();
3189
+ init_qr();
3190
+ init_styles();
3191
+ RACCOON_POLL_MS = 6e4;
3192
+ }));
3193
+
3194
+ //#endregion
3195
+ //#region src/client/agnescode-tab.ts
3196
+ /**
3197
+ * The AgnesCode tab body.
3198
+ * @param {object} props
3199
+ * @param {Tt} props.tt - the dictionary.
3200
+ * @returns {unknown} the tab's card tree.
3201
+ */
3202
+ function AgnescodeTab({ tt }) {
3203
+ const [state, setState] = useState(null);
3204
+ const [loading, setLoading] = useState(true);
3205
+ const [error, setError] = useState(null);
3206
+ const [harvestBusy, setHarvestBusy] = useState(false);
3207
+ const [note, setNote] = useState(null);
3208
+ const alive = useRef(true);
3209
+ const load = useCallback(async () => {
3210
+ try {
3211
+ const response = await fetch(AGNESCODE_PATH, {
3212
+ headers: { accept: "application/json" },
3213
+ cache: "no-store"
3214
+ });
3215
+ if (!response.ok || !alive.current) return;
3216
+ const body = await response.json().catch(() => null);
3217
+ if (!alive.current) return;
3218
+ if (body === null || body.ok === false) {
3219
+ setError(typeof body?.error === "string" && body.error !== "" ? body.error : "no answer");
3220
+ return;
3221
+ }
3222
+ setState(body);
3223
+ setError(null);
3224
+ } catch {
3225
+ if (alive.current) setError("unable to reach the Host");
3226
+ } finally {
3227
+ if (alive.current) setLoading(false);
3228
+ }
3229
+ }, []);
3230
+ useEffect(() => {
3231
+ alive.current = true;
3232
+ load();
3233
+ const timer = setInterval(() => {
3234
+ if (alive.current) load();
3235
+ }, AGNESCODE_POLL_MS);
3236
+ return () => {
3237
+ alive.current = false;
3238
+ clearInterval(timer);
3239
+ };
3240
+ }, [load]);
3241
+ const toggle = useCallback(async (enabled) => {
3242
+ setNote(null);
3243
+ try {
3244
+ const body = await postJsonOrThrow(AGNESCODE_PATH, {
3245
+ action: "switch",
3246
+ enabled
3247
+ });
3248
+ if (alive.current) setState((current) => current ? {
3249
+ ...current,
3250
+ enabled: body.enabled === true,
3251
+ providerRegistered: body.providerRegistered === true
3252
+ } : current);
3253
+ } catch (why) {
3254
+ if (alive.current) setNote(format(tt("agnescode.switchError"), { error: why instanceof Error ? why.message : String(why) }));
3255
+ }
3256
+ }, [tt]);
3257
+ const harvest = useCallback(async () => {
3258
+ setHarvestBusy(true);
3259
+ setNote(null);
3260
+ try {
3261
+ const body = await postJson(AGNESCODE_PATH, { action: "harvest" });
3262
+ if (alive.current) {
3263
+ const attempts = Array.isArray(body?.harvest?.attempts) ? body.harvest.attempts : [];
3264
+ setState((current) => current ? {
3265
+ ...current,
3266
+ harvest: {
3267
+ ok: body?.ok === true,
3268
+ attempts
3269
+ }
3270
+ } : current);
3271
+ if (body?.ok === true) setNote(tt("agnescode.harvestOk"));
3272
+ else setNote(tt("agnescode.harvestFail"));
3273
+ load();
3274
+ }
3275
+ } catch (why) {
3276
+ if (alive.current) setNote(format(tt("agnescode.error"), { error: why instanceof Error ? why.message : String(why) }));
3277
+ } finally {
3278
+ if (alive.current) setHarvestBusy(false);
3279
+ }
3280
+ }, [load, tt]);
3281
+ const logout = useCallback(async () => {
3282
+ setNote(null);
3283
+ try {
3284
+ await postJsonOrThrow(AGNESCODE_PATH, { action: "logout" });
3285
+ if (alive.current) load();
3286
+ } catch (why) {
3287
+ if (alive.current) setNote(format(tt("agnescode.error"), { error: why instanceof Error ? why.message : String(why) }));
3288
+ }
3289
+ }, [load, tt]);
3290
+ const enabled = state?.enabled === true;
3291
+ const loggedIn = state?.loggedIn === true;
3292
+ const models = Array.isArray(state?.models) ? state.models : [];
3293
+ const attempts = Array.isArray(state?.harvest?.attempts) ? state.harvest.attempts : [];
3294
+ const balance = state?.balance ?? null;
3295
+ return h("div", null, h("div", { style: {
3296
+ fontSize: 12,
3297
+ color: "var(--dsw-alias-label-secondary)",
3298
+ marginBottom: 12
3299
+ } }, tt("agnescode.desc")), h("label", { style: {
3300
+ display: "flex",
3301
+ gap: 8,
3302
+ alignItems: "center",
3303
+ margin: "0 0 12px",
3304
+ cursor: harvestBusy ? "wait" : "pointer"
3305
+ } }, h("input", {
3306
+ type: "checkbox",
3307
+ checked: enabled,
3308
+ disabled: harvestBusy,
3309
+ onChange: () => void toggle(!enabled)
3310
+ }), h("span", { style: {
3311
+ fontSize: 12,
3312
+ color: "var(--dsw-alias-label-secondary)"
3313
+ } }, tt("agnescode.switch"))), state !== null ? state.providerRegistered === true ? h("div", {
3314
+ style: {
3315
+ fontSize: 12,
3316
+ color: "var(--dsw-alias-label-secondary)"
3317
+ },
3318
+ role: "status"
3319
+ }, format(tt("agnescode.registered"), { count: count(models.length) })) : state.providerError !== void 0 && state.providerError !== "" ? h("div", {
3320
+ style: S.formError,
3321
+ role: "alert"
3322
+ }, state.providerError) : enabled && !loggedIn ? h("div", { style: {
3323
+ ...S.muted,
3324
+ fontSize: 12
3325
+ } }, tt("agnescode.awaitingHarvest")) : h("div", { style: {
3326
+ ...S.muted,
3327
+ fontSize: 12
3328
+ } }, tt("agnescode.unregistered")) : null, h("div", { style: {
3329
+ ...S.card,
3330
+ marginTop: 4
3331
+ } }, loggedIn ? h("div", null, h("div", {
3332
+ style: { fontSize: 13 },
3333
+ role: "status"
3334
+ }, format(tt("agnescode.loggedIn"), { nick: String(state?.nickname ?? "") })), state?.bffBase !== void 0 && state?.bffBase !== "" ? h("div", { style: {
3335
+ ...S.muted,
3336
+ fontSize: 12,
3337
+ marginTop: 4,
3338
+ wordBreak: "break-all"
3339
+ } }, format(tt("agnescode.bffBase"), { base: state.bffBase })) : null, typeof state?.expiresAtMs === "number" && state.expiresAtMs > 0 ? h("div", { style: {
3340
+ ...S.muted,
3341
+ fontSize: 12,
3342
+ marginTop: 4
3343
+ } }, format(tt("agnescode.expiresAt"), { time: clockLong(state.expiresAtMs) })) : null, h("button", {
3344
+ type: "button",
3345
+ style: {
3346
+ ...S.button,
3347
+ marginTop: 8
3348
+ },
3349
+ onClick: () => void harvest(),
3350
+ disabled: harvestBusy
3351
+ }, harvestBusy ? tt("agnescode.harvesting") : tt("agnescode.harvest")), h("button", {
3352
+ type: "button",
3353
+ style: {
3354
+ ...S.button,
3355
+ marginTop: 8,
3356
+ marginLeft: 8
3357
+ },
3358
+ onClick: () => void logout()
3359
+ }, tt("agnescode.logout"))) : h("div", null, h("div", { style: { fontSize: 13 } }, tt("agnescode.notLogged")), h("button", {
3360
+ type: "button",
3361
+ style: {
3362
+ ...S.button,
3363
+ marginTop: 8
3364
+ },
3365
+ onClick: () => void harvest(),
3366
+ disabled: harvestBusy
3367
+ }, harvestBusy ? tt("agnescode.harvesting") : tt("agnescode.harvest")))), attempts.length > 0 ? h("div", {
3368
+ style: {
3369
+ ...S.muted,
3370
+ fontSize: 12,
3371
+ marginTop: 8
3372
+ },
3373
+ role: "list"
3374
+ }, attempts.map((attempt, index) => h("div", {
3375
+ key: `${String(attempt?.file ?? index)}-${index}`,
3376
+ role: "listitem",
3377
+ style: { marginBottom: 2 }
3378
+ }, h("span", { style: { color: attempt?.tier === "ok" ? "inherit" : "var(--dsw-alias-label-secondary)" } }, `[${tt(`agnescode.tier.${attempt?.tier}`)}] ${String(attempt?.file ?? "")}${attempt?.detail ? ` — ${attempt.detail}` : ""}`)))) : null, note !== null ? h("div", {
3379
+ style: {
3380
+ ...S.formNote,
3381
+ fontSize: 12,
3382
+ marginTop: 8
3383
+ },
3384
+ role: "status"
3385
+ }, note) : null, loggedIn ? h("div", { style: { marginTop: 12 } }, balance !== null ? h("div", { style: {
3386
+ ...S.muted,
3387
+ fontSize: 12,
3388
+ marginBottom: 2
3389
+ } }, format(tt("agnescode.balance"), { balance: count(balance.totalBalance ?? 0) })) : null, balance !== null ? h("div", { style: {
3390
+ ...S.muted,
3391
+ fontSize: 12,
3392
+ marginBottom: 8
3393
+ } }, format(tt("agnescode.balanceDetail"), {
3394
+ timeSensitive: count(balance.timeSensitiveBalance ?? 0),
3395
+ permanent: count(balance.permanentBalance ?? 0)
3396
+ })) : null, models.length > 0 ? h(AgnescodeRoster, {
3397
+ models,
3398
+ tt
3399
+ }) : null) : null);
3400
+ }
3401
+ /**
3402
+ * The model roster the AgnesCode adapter offers, as a hook-free component.
3403
+ *
3404
+ * Split out of {@link AgnescodeTab} for the same reason `RaccoonRoster` is:
3405
+ * the tab's data is internal state, so the render suite can only ever reach
3406
+ * the unlinked frame — a roster inlined there is unassertable. The row is the
3407
+ * SAME two-line shape as the sibling rosters (head line over an indented
3408
+ * parameter line), sharing `S.modelRow`'s contract. What it carries instead
3409
+ * of the Raccoon rate chip is the `memberOnly` badge — the platform-declared
3410
+ * gating fact this provider HAS; no multiplier chip exists here because the
3411
+ * upstream declares no per-model rate (billing is the credit pool), and
3412
+ * inventing one would libel the roster.
3413
+ * @param {object} props
3414
+ * @param {AgnescodeModel[]} props.models - the rows the route reported.
3415
+ * @param {import("./runtime.ts").Tt} props.tt - the dictionary.
3416
+ * @returns {unknown} the roster list element.
3417
+ */
3418
+ function AgnescodeRoster({ models, tt }) {
3419
+ const rows = Array.isArray(models) ? models : [];
3420
+ return h("div", { style: S.modelPanel }, h("div", { style: {
3421
+ ...S.muted,
3422
+ fontSize: 12,
3423
+ marginBottom: 6
3424
+ } }, format(tt("agnescode.models"), { count: count(rows.length) })), h("ul", {
3425
+ style: S.modelList,
3426
+ role: "list"
3427
+ }, rows.map((row) => {
3428
+ const id = String(row?.id ?? "");
3429
+ const label = String(row?.name ?? id);
3430
+ const meta = [typeof row?.contextWindow === "number" && row.contextWindow > 0 ? format(tt("llm.contextBadge"), { ctx: tokenSize(row.contextWindow) }) : null, typeof row?.maxOutputLength === "number" && row.maxOutputLength > 0 ? format(tt("llm.metaOutput"), { out: tokenSize(row.maxOutputLength) }) : null].filter(Boolean).join(" · ");
3431
+ return h("li", {
3432
+ key: id,
3433
+ style: S.modelRow
3434
+ }, h("div", { style: S.modelRowHead }, h("span", {
3435
+ style: S.modelName,
3436
+ title: id
3437
+ }, label), h("span", { style: S.spacer }), row.memberOnly === true ? h("span", { style: S.modelBadge }, tt("agnescode.memberOnly")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
3438
+ })));
3439
+ }
3440
+ var AGNESCODE_POLL_MS;
3441
+ var init_agnescode_tab = __esmMin((() => {
3442
+ init_const();
3443
+ init_format();
3444
+ init_http();
3445
+ init_runtime();
3446
+ init_styles();
3447
+ AGNESCODE_POLL_MS = 6e4;
3448
+ }));
3449
+
3450
+ //#endregion
3451
+ //#region src/client/panel-page.ts
3452
+ function PanelPage({ onClose, tt, localeSubscribe }) {
3453
+ const [data, setData] = useState(null);
3454
+ const [error, setError] = useState(null);
3455
+ const [loadedOnce, setLoadedOnce] = useState(false);
3456
+ const [updatedAt, setUpdatedAt] = useState(0);
3457
+ const [, setLocaleRevision] = useState(0);
3458
+ const [openSections, setOpenSections] = useState({
3459
+ quota: true,
3460
+ usage: true,
3461
+ account: false,
3462
+ provider: true,
3463
+ draw: true,
3464
+ video: true,
3465
+ llm: false
3466
+ });
3467
+ const [activeTab, setActiveTab] = useState("quota");
3468
+ useEffect(() => {
3469
+ if (typeof localeSubscribe !== "function") return void 0;
3470
+ return localeSubscribe(() => setLocaleRevision((revision) => revision + 1));
3471
+ }, [localeSubscribe]);
3472
+ const [cadenceMs, setCadenceMs] = useState(3e4);
3473
+ const generation = useRef(0);
3474
+ const inFlight = useRef(null);
3475
+ const load = useCallback(async () => {
3476
+ generation.current += 1;
3477
+ const mine = generation.current;
3478
+ const isCurrent = () => generation.current === mine;
3479
+ inFlight.current?.abort?.();
3480
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
3481
+ inFlight.current = controller;
3482
+ try {
3483
+ const response = await fetch(SNAPSHOT_PATH, {
3484
+ headers: { accept: "application/json" },
3485
+ cache: "no-store",
3486
+ signal: controller ? controller.signal : void 0
3487
+ });
3488
+ if (!isCurrent()) return;
3489
+ if (!response.ok) {
3490
+ setError(errorOfStatus(response.status));
3491
+ return;
3492
+ }
3493
+ const body = await response.json();
3494
+ if (!isCurrent()) return;
3495
+ const read = interpretSnapshot(body);
3496
+ if (read.data === null) {
3497
+ setData(null);
3498
+ setError(read.error);
3499
+ return;
3500
+ }
3501
+ setData(read.data);
3502
+ setError(null);
3503
+ setUpdatedAt(Date.now());
3504
+ const stated = read.data?.pollSeconds;
3505
+ if (typeof stated === "number" && Number.isFinite(stated)) setCadenceMs(Math.min(3600, Math.max(5, Math.floor(stated))) * 1e3);
3506
+ } catch (reason) {
3507
+ if (!isCurrent()) return;
3508
+ setError(reason instanceof Error ? reason.message : String(reason));
3509
+ } finally {
3510
+ if (isCurrent()) setLoadedOnce(true);
3511
+ if (inFlight.current === controller) inFlight.current = null;
3512
+ }
3513
+ }, []);
3514
+ const toggleSection = useCallback((key) => {
3515
+ setOpenSections((current) => ({
3516
+ ...current,
3517
+ [key]: !current[key]
3518
+ }));
3519
+ }, []);
3520
+ useEffect(() => {
3521
+ let alive = true;
3522
+ let timer = null;
3523
+ const run = () => {
3524
+ if (alive) load();
3525
+ };
3526
+ const start = () => {
3527
+ if (timer === null) timer = setInterval(run, cadenceMs);
3528
+ };
3529
+ const stop = () => {
3530
+ if (timer !== null) {
3531
+ clearInterval(timer);
3532
+ timer = null;
3533
+ }
3534
+ };
3535
+ run();
3536
+ start();
3537
+ const onVisibility = () => {
3538
+ if (!alive) return;
3539
+ if (document.visibilityState === "hidden") stop();
3540
+ else {
3541
+ run();
3542
+ start();
3543
+ }
3544
+ };
3545
+ if (typeof document !== "undefined" && document.addEventListener) document.addEventListener("visibilitychange", onVisibility);
3546
+ return () => {
3547
+ alive = false;
3548
+ stop();
3549
+ if (typeof document !== "undefined" && document.addEventListener) document.removeEventListener("visibilitychange", onVisibility);
3550
+ };
3551
+ }, [load, cadenceMs]);
3552
+ const openedAccountOnce = useRef(false);
3553
+ useEffect(() => {
3554
+ if (openedAccountOnce.current) return;
3555
+ if (data?.quota?.consoleConnected !== false) return;
3556
+ openedAccountOnce.current = true;
3557
+ setOpenSections((current) => ({
3558
+ ...current,
3559
+ account: true
3560
+ }));
3561
+ }, [data]);
3562
+ const quota = data?.quota;
3563
+ const usage = data?.usage;
3564
+ const { failure, auth, needsSetup, guidance, shapeWarnings } = viewOf(data, error, tt);
3565
+ const showSetupForm = needsSetup && loadedOnce;
3566
+ const authChip = auth === null ? null : auth.error || !auth.configured ? h("span", {
3567
+ style: S.chip,
3568
+ title: auth.error || guidance || ""
3569
+ }, tt("auth.needsLogin")) : h("span", { style: S.chip }, tt("auth.selfRenew"));
3570
+ const authManage = auth !== null;
3571
+ const tabBar = h("div", {
3572
+ style: S.tabBar,
3573
+ role: "tablist"
3574
+ }, h("button", {
3575
+ type: "button",
3576
+ role: "tab",
3577
+ "aria-selected": activeTab === "quota",
3578
+ style: {
3579
+ ...S.tab,
3580
+ ...activeTab === "quota" ? S.tabActive : {}
3581
+ },
3582
+ onClick: () => setActiveTab("quota")
3583
+ }, tt("tab.quota")), h("button", {
3584
+ type: "button",
3585
+ role: "tab",
3586
+ "aria-selected": activeTab === "api",
3587
+ style: {
3588
+ ...S.tab,
3589
+ ...activeTab === "api" ? S.tabActive : {}
3590
+ },
3591
+ onClick: () => setActiveTab("api")
3592
+ }, tt("tab.api")), h("button", {
3593
+ type: "button",
3594
+ role: "tab",
3595
+ "aria-selected": activeTab === "raccoon",
3596
+ style: {
3597
+ ...S.tab,
3598
+ ...activeTab === "raccoon" ? S.tabActive : {}
3599
+ },
3600
+ onClick: () => setActiveTab("raccoon")
3601
+ }, tt("tab.raccoon")), h("button", {
3602
+ type: "button",
3603
+ role: "tab",
3604
+ "aria-selected": activeTab === "agnescode",
3605
+ style: {
3606
+ ...S.tab,
3607
+ ...activeTab === "agnescode" ? S.tabActive : {}
3608
+ },
3609
+ onClick: () => setActiveTab("agnescode")
3610
+ }, tt("tab.agnescode")));
3611
+ const quotaPlaceholder = showSetupForm ? h(AccountForm, {
3612
+ auth,
3613
+ onDone: () => void load(),
3614
+ tt,
3615
+ bare: true,
3616
+ hasSnapshot: false
3617
+ }) : h("div", { style: S.empty }, failure === null ? tt("panel.loading") : h("div", null, h("div", { role: "alert" }, guidance ?? format(tt("panel.error"), { error: failure.message }))));
3618
+ const body = h("div", null, tabBar, activeTab === "quota" && shapeWarnings.length > 0 ? h("div", {
3619
+ style: S.formError,
3620
+ role: "status"
3621
+ }, format(tt("panel.shapeDrift"), { detail: shapeWarnings.map((entry) => `${tt("shape.api")} ${entry.api} ${tt("shape.missing")} ${entry.missing}`).join("; ") })) : null, activeTab === "quota" ? data === null ? quotaPlaceholder : h("div", null, h(SectionCard, {
3622
+ title: tt("section.quota"),
3623
+ open: openSections.quota,
3624
+ onToggle: () => toggleSection("quota"),
3625
+ tt
3626
+ }, quota?.consoleConnected === false ? h("div", {
3627
+ style: {
3628
+ ...S.formNote,
3629
+ marginTop: 0,
3630
+ marginBottom: 12
3631
+ },
3632
+ role: "status"
3633
+ }, h("div", null, guidance ?? tt("quota.consoleOffline")), needsSetup ? h("div", { style: { marginTop: 6 } }, format(tt("quota.consoleOfflineHint"), { section: tt("auth.title") })) : null) : quota?.error ? h("div", {
3634
+ style: {
3635
+ ...S.formNote,
3636
+ marginTop: 0,
3637
+ marginBottom: 12
3638
+ },
3639
+ role: "status"
3640
+ }, format(tt("quota.error"), {
3641
+ source: String(quota.error.source ?? ""),
3642
+ message: String(quota.error.message ?? "")
3643
+ })) : null, h(PlanCard, {
3644
+ quota,
3645
+ tt
3646
+ }), h("div", { style: {
3647
+ ...S.muted,
3648
+ fontSize: 12,
3649
+ marginTop: 12
3650
+ } }, tt("quota.windowNote")), h(UsageTotals, {
3651
+ totals: quota?.totals,
3652
+ label: tt("quota.accountTotals"),
3653
+ tt
3654
+ }), Array.isArray(data.visionModels) && data.visionModels.length > 0 ? h("div", { style: {
3655
+ ...S.muted,
3656
+ fontSize: 12,
3657
+ marginTop: 10
3658
+ } }, format(tt("pool.vision"), { models: data.visionModels.map((entry) => entry.id).join(" · ") + (data.visionModels.every((entry) => entry.source === "name") ? tt("pool.visionInferred") : "") })) : null), h(SectionCard, {
3659
+ title: format(tt("section.usage"), { days: usage?.days ?? 30 }),
3660
+ open: openSections.usage,
3661
+ onToggle: () => toggleSection("usage"),
3662
+ tt
3663
+ }, h(UsageChart, {
3664
+ usage,
3665
+ tt
3666
+ })), h("div", { style: S.note }, format(tt("note"), { cache: data?.cacheSeconds ?? 60 })), authManage ? h(SectionCard, {
3667
+ title: tt("auth.title"),
3668
+ open: openSections.account,
3669
+ onToggle: () => toggleSection("account"),
3670
+ tt
3671
+ }, h(AccountForm, {
3672
+ auth,
3673
+ onDone: () => void load(),
3674
+ tt,
3675
+ bare: true,
3676
+ hasSnapshot: true
3677
+ })) : null) : activeTab === "raccoon" ? h("div", { style: { marginTop: 22 } }, h(SectionCard, {
3678
+ title: tt("raccoon.title"),
3679
+ open: true,
3680
+ onToggle: () => {},
3681
+ tt
3682
+ }, h(RaccoonTab, { tt }))) : activeTab === "agnescode" ? h("div", { style: { marginTop: 22 } }, h(SectionCard, {
3683
+ title: tt("agnescode.title"),
3684
+ open: true,
3685
+ onToggle: () => {},
3686
+ tt
3687
+ }, h(AgnescodeTab, { tt }))) : h("div", null, h(SectionCard, {
3688
+ title: tt("llm.providerTitle"),
3689
+ open: openSections.provider,
3690
+ onToggle: () => toggleSection("provider"),
3691
+ tt
3692
+ }, h(ProviderForm, {
3693
+ llm: data?.llm ?? null,
3694
+ onDone: () => void load(),
3695
+ tt
3696
+ })), h(SectionCard, {
3697
+ title: tt("draw.title"),
3698
+ open: openSections.draw,
3699
+ onToggle: () => toggleSection("draw"),
3700
+ tt
3701
+ }, h(DrawSwitch, {
3702
+ llm: data?.llm ?? null,
3703
+ onDone: () => void load(),
3704
+ tt
3705
+ })), h(SectionCard, {
3706
+ title: tt("video.title"),
3707
+ open: openSections.video,
3708
+ onToggle: () => toggleSection("video"),
3709
+ tt
3710
+ }, h(VideoSwitch, {
3711
+ llm: data?.llm ?? null,
3712
+ onDone: () => void load(),
3713
+ tt
3714
+ })), h(SectionCard, {
3715
+ title: tt("llm.title"),
3716
+ open: openSections.llm,
3717
+ onToggle: () => toggleSection("llm"),
3718
+ tt
3719
+ }, h(ApiKeyForm, {
3720
+ llm: data?.llm ?? null,
3721
+ onDone: () => void load(),
3722
+ tt,
3723
+ bare: true
3724
+ }))));
3725
+ return h("div", {
3726
+ style: S.page,
3727
+ "data-dsh-plugin": "dsh-connect-agnes-token-plan"
3728
+ }, h("div", { style: S.headerBar }, h("div", { style: S.header }, h("h1", { style: S.title }, tt("panel.title")), h("span", { style: S.updated }, data ? format(tt("panel.updated"), { time: clock(updatedAt / 1e3) }) : ""), authChip, h("span", { style: S.spacer }), failure && data ? h("span", {
3729
+ style: S.error,
3730
+ role: "status",
3731
+ title: failure.message
3732
+ }, format(tt("panel.error"), { error: failure.message })) : null, h("button", {
3733
+ type: "button",
3734
+ style: S.button,
3735
+ onClick: () => void load()
3736
+ }, tt("panel.refresh")), onClose ? h("button", {
3737
+ type: "button",
3738
+ style: S.button,
3739
+ onClick: () => onClose()
3740
+ }, tt("panel.back")) : null)), h("div", { style: S.scroll }, h("div", { style: S.content }, body)));
3741
+ }
3742
+ var init_panel_page = __esmMin((() => {
3743
+ init_account_form();
3744
+ init_api_key_form();
3745
+ init_const();
3746
+ init_format();
3747
+ init_snapshot();
3748
+ init_runtime();
3749
+ init_styles();
3750
+ init_cards();
3751
+ init_provider_controls();
3752
+ init_raccoon_tab();
3753
+ init_agnescode_tab();
3754
+ }));
3755
+
3756
+ //#endregion
3757
+ //#region src/client/apply.ts
3758
+ /**
3759
+ * Register the dictionaries and the plugin config card.
3760
+ *
3761
+ * The card is rendered inside the Plugins page by the Host's
3762
+ * `renderSlot("plugins.bundle.config", …)` call. No `onClose` is passed —
3763
+ * the Plugins page owns navigation; the card has no close button.
3764
+ */
3765
+ function apply(ctx) {
3766
+ ctx.effect(() => {
3767
+ try {
3768
+ return ctx.locale.register(NS, {
3769
+ zh,
3770
+ en
3771
+ });
3772
+ } catch {
3773
+ return () => {};
3774
+ }
3775
+ }, "dsh-connect-agnes-token-plan: dictionaries");
3776
+ let translate = (key) => key;
3777
+ try {
3778
+ translate = ctx.locale.bind(NS);
3779
+ } catch {}
3780
+ const tt = (key) => {
3781
+ try {
3782
+ return translate(key);
3783
+ } catch {
3784
+ return key;
3785
+ }
3786
+ };
3787
+ const disposers = [];
3788
+ try {
3789
+ disposers.push(ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
3790
+ name: "plugins.bundle.config",
3791
+ key: NS,
3792
+ locale: NS,
3793
+ inject: () => ({
3794
+ tt,
3795
+ localeSubscribe: ctx.locale.subscribe.bind(ctx.locale)
3796
+ })
3797
+ }, PanelPage)));
3798
+ } catch (error) {
3799
+ console.warn("[dsh-connect-agnes-token-plan] config card registration failed:", error);
3800
+ }
3801
+ ctx.effect(() => () => {
3802
+ for (const dispose of disposers.splice(0)) try {
3803
+ dispose();
3804
+ } catch {}
3805
+ }, "dsh-connect-agnes-token-plan: ui mounts");
3806
+ }
3807
+ var inject;
3808
+ var init_apply = __esmMin((() => {
3809
+ init_const();
3810
+ init_i18n();
3811
+ init_panel_page();
3812
+ inject = ["slots", "locale"];
3813
+ }));
3814
+
3815
+ //#endregion
3816
+ //#region src/client/index.ts
3817
+ var require_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3818
+ init_apply();
3819
+ init_snapshot();
3820
+ init_models();
3821
+ init_format();
3822
+ init_runtime();
3823
+ init_i18n();
3824
+ init_styles();
3825
+ init_account_form();
3826
+ init_cards();
3827
+ init_provider_controls();
3828
+ init_api_key_form();
3829
+ init_model_picker();
3830
+ init_panel_page();
3831
+ init_raccoon_tab();
3832
+ init_agnescode_tab();
3833
+ init_qr();
3834
+ function clientFactory(loaderRequire) {
3835
+ provideClientReact(loaderRequire("react"));
3836
+ /**
3837
+ * The module's test surface.
3838
+ *
3839
+ * The Host only ever reads `inject`/`apply`; this object exists so the
3840
+ * Node-side suites can load the shipped bundle as a module and exercise
3841
+ * these REAL definitions — the decision, the dictionaries, the style
3842
+ * tokens, the components — instead of scraping the source text for them.
3843
+ * Everything here is what the browser itself uses; nothing is defined for
3844
+ * the tests' benefit.
3845
+ */
3846
+ const panel = Object.freeze({
3847
+ interpretSnapshot,
3848
+ viewOf,
3849
+ errorOfStatus,
3850
+ dictionaries: Object.freeze({
3851
+ zh,
3852
+ en
3853
+ }),
3854
+ tables: Object.freeze({
3855
+ GUIDANCE_BY_CODE,
3856
+ FORM_EXCLUDED_CODES,
3857
+ REFUSAL_TEXT
3858
+ }),
3859
+ styles: S,
3860
+ helpers: Object.freeze({
3861
+ clock,
3862
+ clockLong,
3863
+ count,
3864
+ format,
3865
+ tokenSize,
3866
+ HIDE_ALL_MODELS,
3867
+ modelIsOn,
3868
+ allowListFor,
3869
+ toggleModelIn,
3870
+ setAllModelsIn,
3871
+ bulkModelsIn
3872
+ }),
3873
+ components: Object.freeze({
3874
+ PlanCard,
3875
+ QuotaWindowCard,
3876
+ UsageTotals,
3877
+ UsageChart,
3878
+ SectionCard,
3879
+ AccountForm,
3880
+ ApiKeyForm,
3881
+ ProviderForm,
3882
+ ProviderStatus,
3883
+ ProviderRegStatus,
3884
+ ProviderSwitch,
3885
+ DrawSwitch,
3886
+ VideoSwitch,
3887
+ ModelRoster,
3888
+ ModelPicker,
3889
+ PanelPage,
3890
+ RaccoonTab,
3891
+ RaccoonRoster,
3892
+ AgnescodeTab,
3893
+ AgnescodeRoster
3894
+ }),
3895
+ qr: Object.freeze({
3896
+ buildQrMatrix,
3897
+ qrDataUrl
3898
+ })
3899
+ });
3900
+ return {
3901
+ inject,
3902
+ apply,
3903
+ panel
3904
+ };
3905
+ }
3906
+ /** The registration the Host loads: id plus the factory the Host materializes. */
3907
+ const REGISTRATION = {
3908
+ id: "dsh-connect-agnes-token-plan",
3909
+ factory: clientFactory
3910
+ };
3911
+ if (typeof window !== "undefined") {
3912
+ const loader = window.__ModuleLoader__;
3913
+ if (loader !== void 0) loader.load(REGISTRATION);
3914
+ }
3915
+ if (typeof module !== "undefined" && module !== null && module.exports !== void 0) module.exports = REGISTRATION;
3916
+ }));
3917
+
3918
+ //#endregion
3919
+ return require_client();
3920
+
3921
+ })();