dsh-cliproxyapi-pro 0.0.0-stage → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1867 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-cliproxyapi-pro",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+
8
+ const React = require("react");
9
+ const h = React.createElement;
10
+ const { useState, useEffect, useCallback, useMemo } = React;
11
+
12
+ const PACKAGE_NAME = "dsh-cliproxyapi-pro";
13
+ const MODULE_ID = "dsh-cliproxyapi-pro";
14
+ const NS = "dsh-cliproxyapi-pro";
15
+
16
+ // Document-relative API paths (compatible with <base href="./"> in DSH Web/Desktop)
17
+ const API = {
18
+ status: "api/dsh-cliproxyapi/status",
19
+ refresh: "api/dsh-cliproxyapi/refresh",
20
+ save: "api/dsh-cliproxyapi/save",
21
+ addAccount: "api/dsh-cliproxyapi/accounts/add",
22
+ removeAccount: "api/dsh-cliproxyapi/accounts/remove",
23
+ };
24
+
25
+ const zh = {
26
+ nav: "CLIProxyAPI",
27
+ title: "CLIProxyAPI 自动发现与模型筛选",
28
+ subtitle:
29
+ "自动连接 CLIProxyAPI 服务,勾选你希望在对话框模型选择器中显示的模型,自动同步上下文长度、视觉与推理档位。",
30
+ statusConnected: "已连接并同步",
31
+ statusProbing: "正在探测同步…",
32
+ statusError: "连接异常",
33
+ statusDisabled: "已停用",
34
+ statusIdle: "等待同步",
35
+ endpointLabel: "当前活动节点",
36
+ endpointNone: "未连接(将依次尝试配置地址与本地 8317 端口)",
37
+ modelsCountLabel: "选择器展示模型",
38
+ refreshedAtLabel: "上次同步时间",
39
+ refreshBtn: "立即探测刷新",
40
+ refreshingBtn: "正在探测…",
41
+ saveBtn: "保存设置并应用",
42
+ savingBtn: "正在应用…",
43
+ saveSelectionBtn: "保存模型勾选",
44
+ unsavedChanges: "有未保存的勾选更改",
45
+ savedToast: "已保存并同步到对话框模型选择器",
46
+ sectionConnection: "连接与认证配置",
47
+ accountsLabel: "账号",
48
+ addAccountBtn: "+ 添加账号",
49
+ addingAccountBtn: "正在添加…",
50
+ removeAccountBtn: "删除此账号",
51
+ removeConfirm: "确定删除账号「{name}」吗?它同步到模型选择器中的模型路由也会一并移除(已保存的密钥不会删除)。",
52
+ noAccounts: "还没有任何账号。点击「添加账号」接入一个 CLIProxyAPI 服务。",
53
+ accountOffBadge: "未启用",
54
+ addedToast: "已添加新账号(默认未启用)。填写服务地址与密钥后,打开「启用此账号」并保存。",
55
+ removedToast: "账号已删除,其模型已从模型选择器中移除",
56
+ disabledHint:
57
+ "该账号未启用,它的模型不会出现在对话框模型选择器中。可先填写地址与密钥并点击「立即探测刷新」预览模型列表,勾选后再开启「启用此账号」。",
58
+ enabledLabel: "启用此账号",
59
+ enabledHint: "开启后将该账号下勾选的模型同步到 DSH 对话框模型选择器;关闭后保留配置,但移除该账号的模型。",
60
+ baseUrlLabel: "CLIProxyAPI 服务地址 (Base URL)",
61
+ baseUrlPlaceholder: "留空则自动继承现有路由或探测 http://127.0.0.1:8317",
62
+ baseUrlHint: "支持填写根地址(如 http://127.0.0.1:8317 或远程 IP:8317),无需手动拼接 /v1。",
63
+ apiKeyLabel: "API Key 密钥",
64
+ apiKeySetPlaceholder: "已配置密钥(留空保持不变,输入新值可覆盖)",
65
+ apiKeyUnsetPlaceholder: "输入 CLIProxyAPI 访问密钥(本地免密模式可留空)",
66
+ apiKeyHint: "密钥通过 DSH 凭证服务安全存储在 .credentials.yaml 中,绝不明文写入配置文件。",
67
+ apiKeyConfiguredBadge: "密钥已配置",
68
+ apiKeyUnsetBadge: "未设置密钥",
69
+ clearKeyBtn: "清除密钥",
70
+ protocolLabel: "接口协议",
71
+ protocolHint: "自动模式下每个模型走所属家族的原生协议,减少 CPA 中转转换;Gemini 等 DSH 不支持原生协议的模型走 OpenAI Chat。",
72
+ protocolAuto: "自动:按模型家族走原生协议(推荐)",
73
+ protocolFixedPrefix: "统一使用:",
74
+ protocolNameCompletions: "OpenAI Chat",
75
+ protocolNameResponses: "OpenAI Responses",
76
+ protocolNameMessages: "Claude Messages",
77
+ modelProtocolLabel: "请求协议",
78
+ modelProtocolDefault: "跟随全局设置",
79
+ modelProtocolHint: "原生协议:",
80
+ sectionAdvanced: "高级路由与规则过滤",
81
+ providerIdLabel: "DSH 路由 ID (Provider)",
82
+ providerIdHint: "写入 llm-pi-ai 的提供商路由标识,每个账号必须不同(首个账号默认 cli-proxy,与现有配置无缝衔接)。",
83
+ apiKeyEnvLabel: "密钥引用名 (Credential Ref)",
84
+ apiKeyEnvHint: "密钥在 DSH 凭证服务中的名称,每个账号建议使用不同的引用名。",
85
+ displayNameLabel: "账号名称 (Display Name)",
86
+ displayNameHint: "用于区分账号,并作为 DSH 对话框模型选择器中的分组名称。",
87
+ includePrefixLabel: "仅包含模型关键词 (可选)",
88
+ includePrefixPlaceholder: "例如: gpt-6, claude-opus, gemini-4 (多个用逗号分隔)",
89
+ includePrefixHint: "附加规则:只允许模型 ID 包含指定关键词的模型。",
90
+ excludePrefixLabel: "排除模型关键词 (可选)",
91
+ excludePrefixPlaceholder: "例如: tts, image, video, zj/ (多个用逗号分隔)",
92
+ excludePrefixHint: "附加规则:排除包含指定关键词的模型 ID。",
93
+ refreshIntervalLabel: "自动刷新间隔 (秒,所有账号共用)",
94
+ sectionModels: "模型显示筛选(仅勾选的模型会在回答选择器中显示)",
95
+ sectionModelsHint:
96
+ "点击模型行即可勾选或取消。不勾选任何模型(全部清空)时默认不限制;勾选后仅展示选中的模型。",
97
+ searchModelsPlaceholder: "搜索模型 ID 或名称(如 claude-opus, gpt-6, gemini-4, swe)…",
98
+ filterAll: "全部发现",
99
+ filterSelected: "已勾选",
100
+ filterUnselected: "未勾选",
101
+ familyAll: "全部家族",
102
+ selectAllVisibleBtn: "全选当前列表",
103
+ deselectAllVisibleBtn: "取消当前列表",
104
+ selectRecommendedBtn: "恢复推荐精选",
105
+ selectRecommendedHint: "按该服务的模型目录,每个模型家族勾选最新的一个",
106
+ badgeVision: "视觉 Image",
107
+ badgeReasoning: "推理",
108
+ badgeSelected: "已启用",
109
+ contextWindowTag: "上下文",
110
+ noModelsText: "未找到匹配的模型。请检查搜索条件或点击上方「立即探测刷新」。",
111
+ editParamsBtn: "编辑参数",
112
+ editingParamsBtn: "收起参数",
113
+ customParamsBadge: "已自定义",
114
+ contextWindowLabel: "上下文窗口 (tokens)",
115
+ maxTokensLabel: "最大输出 (tokens)",
116
+ imageInputLabel: "支持图片输入",
117
+ reasoningLevelsLabel: "思考等级",
118
+ resetParamsBtn: "恢复自动探测",
119
+ paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。修改后请点击「保存模型勾选」才会写入模型选择器。",
120
+ discoveredValue: "探测值",
121
+ };
122
+
123
+ const en = {
124
+ nav: "CLIProxyAPI",
125
+ title: "CLIProxyAPI Auto-Discovery & Model Filter",
126
+ subtitle:
127
+ "Connect to CLIProxyAPI and check the exact models you want displayed in the DSH conversation model picker.",
128
+ statusConnected: "Connected & Synced",
129
+ statusProbing: "Probing…",
130
+ statusError: "Connection Error",
131
+ statusDisabled: "Disabled",
132
+ statusIdle: "Idle",
133
+ endpointLabel: "Active Endpoint",
134
+ endpointNone: "Not connected (tries configured URL then localhost:8317)",
135
+ modelsCountLabel: "Models in Picker",
136
+ refreshedAtLabel: "Last Synced",
137
+ refreshBtn: "Probe & Refresh",
138
+ refreshingBtn: "Probing…",
139
+ saveBtn: "Save & Apply",
140
+ savingBtn: "Applying…",
141
+ saveSelectionBtn: "Apply Selected Models",
142
+ unsavedChanges: "Unsaved selection changes",
143
+ savedToast: "Saved and synchronized to the DSH model picker",
144
+ sectionConnection: "Connection & Authentication",
145
+ accountsLabel: "Accounts",
146
+ addAccountBtn: "+ Add Account",
147
+ addingAccountBtn: "Adding…",
148
+ removeAccountBtn: "Delete Account",
149
+ removeConfirm: "Delete account \"{name}\"? Its model routes are removed from the model picker too (the stored key is kept).",
150
+ noAccounts: "No accounts yet. Click Add Account to connect a CLIProxyAPI server.",
151
+ accountOffBadge: "Off",
152
+ addedToast: "Account added (disabled). Fill in its URL and key, then turn on Enable This Account and save.",
153
+ removedToast: "Account deleted and its models removed from the picker",
154
+ disabledHint:
155
+ "This account is disabled, so its models are not in the model picker. Fill in URL and key, click Probe & Refresh to preview its models, check the ones you want, then turn on Enable This Account.",
156
+ enabledLabel: "Enable This Account",
157
+ enabledHint: "Sync this account's checked models into the DSH model selector. Turning it off keeps the settings but removes its models.",
158
+ baseUrlLabel: "CLIProxyAPI Base URL",
159
+ baseUrlPlaceholder: "Leave blank to inherit existing route or auto-discover 127.0.0.1:8317",
160
+ baseUrlHint: "Root URL (e.g. http://127.0.0.1:8317); /v1 is handled automatically.",
161
+ apiKeyLabel: "API Key",
162
+ apiKeySetPlaceholder: "Key is stored (leave blank to keep, or type a new key)",
163
+ apiKeyUnsetPlaceholder: "Enter CLIProxyAPI Bearer key (optional for local no-auth)",
164
+ apiKeyHint: "Stored securely via DSH credentials service, never in plain settings.",
165
+ apiKeyConfiguredBadge: "Configured",
166
+ apiKeyUnsetBadge: "Not Set",
167
+ clearKeyBtn: "Clear Key",
168
+ protocolLabel: "Wire Protocol",
169
+ protocolHint: "Auto sends each model over its family's own protocol to avoid CPA translation; models DSH cannot speak natively (such as Gemini) use OpenAI Chat.",
170
+ protocolAuto: "Auto: native protocol per model family (recommended)",
171
+ protocolFixedPrefix: "Always use: ",
172
+ protocolNameCompletions: "OpenAI Chat",
173
+ protocolNameResponses: "OpenAI Responses",
174
+ protocolNameMessages: "Claude Messages",
175
+ modelProtocolLabel: "Request Protocol",
176
+ modelProtocolDefault: "Follow global setting",
177
+ modelProtocolHint: "Native protocol: ",
178
+ sectionAdvanced: "Advanced Routing & Keyword Rules",
179
+ providerIdLabel: "Provider Route ID",
180
+ providerIdHint: "Provider key under llm-pi-ai; must differ per account (first account: cli-proxy).",
181
+ apiKeyEnvLabel: "Credential Ref",
182
+ apiKeyEnvHint: "Name of the key in the DSH credentials service; use a distinct one per account.",
183
+ displayNameLabel: "Account Name",
184
+ displayNameHint: "Tells accounts apart and titles their group in the DSH model selector.",
185
+ includePrefixLabel: "Include Filter (optional)",
186
+ includePrefixPlaceholder: "e.g. gpt-6, claude-opus, gemini-4 (comma-separated)",
187
+ includePrefixHint: "Additional rule: only sync models matching these substrings.",
188
+ excludePrefixLabel: "Exclude Filter (optional)",
189
+ excludePrefixPlaceholder: "e.g. tts, image, video, zj/ (comma-separated)",
190
+ excludePrefixHint: "Additional rule: hide models matching any of these substrings.",
191
+ refreshIntervalLabel: "Auto Refresh Interval (s, all accounts)",
192
+ sectionModels: "Model Picker Filter (Only checked models appear in DSH)",
193
+ sectionModelsHint:
194
+ "Click any model row to toggle selection. Only checked models will be shown in the conversation model selector.",
195
+ searchModelsPlaceholder: "Search model ID or name (e.g. claude-opus, gpt-6, gemini-4)…",
196
+ filterAll: "All Discovered",
197
+ filterSelected: "Checked",
198
+ filterUnselected: "Unchecked",
199
+ familyAll: "All Families",
200
+ selectAllVisibleBtn: "Check Visible",
201
+ deselectAllVisibleBtn: "Uncheck Visible",
202
+ selectRecommendedBtn: "Reset to Recommended",
203
+ selectRecommendedHint: "Check the newest model of each family in this server's catalog",
204
+ badgeVision: "Image",
205
+ badgeReasoning: "Reasoning",
206
+ badgeSelected: "Active",
207
+ contextWindowTag: "Context",
208
+ noModelsText: "No matching models found. Check your filter or click Probe & Refresh.",
209
+ editParamsBtn: "Edit Parameters",
210
+ editingParamsBtn: "Hide Parameters",
211
+ customParamsBadge: "Custom",
212
+ contextWindowLabel: "Context Window (tokens)",
213
+ maxTokensLabel: "Max Output (tokens)",
214
+ imageInputLabel: "Image Input",
215
+ reasoningLevelsLabel: "Thinking Levels",
216
+ resetParamsBtn: "Reset to Discovered",
217
+ paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI. Click Apply Selected Models to write changes to the picker.",
218
+ discoveredValue: "Discovered",
219
+ };
220
+
221
+ const CSS = `
222
+ .dcpa-wrap {
223
+ max-width: 760px;
224
+ color: var(--dsw-alias-label-primary);
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 14px;
228
+ font-size: 13px;
229
+ line-height: 20px;
230
+ }
231
+ .dcpa-card {
232
+ border: 0.5px solid var(--dsw-alias-border-l2);
233
+ background: var(--dsw-alias-bg-layer-1);
234
+ border-radius: 12px;
235
+ padding: 14px 16px;
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 12px;
239
+ }
240
+ .dcpa-head {
241
+ display: flex;
242
+ align-items: flex-start;
243
+ justify-content: space-between;
244
+ gap: 12px;
245
+ flex-wrap: wrap;
246
+ }
247
+ .dcpa-title {
248
+ margin: 0;
249
+ font-size: 15px;
250
+ font-weight: 600;
251
+ line-height: 22px;
252
+ color: var(--dsw-alias-label-primary);
253
+ }
254
+ .dcpa-subtitle {
255
+ margin: 4px 0 0;
256
+ font-size: 12px;
257
+ line-height: 18px;
258
+ color: var(--dsw-alias-label-secondary);
259
+ }
260
+ .dcpa-status-bar {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 16px;
264
+ flex-wrap: wrap;
265
+ padding: 10px 12px;
266
+ border-radius: 8px;
267
+ background: var(--dsw-alias-bg-layer-2);
268
+ border: 0.5px solid var(--dsw-alias-border-l1);
269
+ }
270
+ .dcpa-status-pill {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 6px;
274
+ font-weight: 500;
275
+ font-size: 12px;
276
+ }
277
+ .dcpa-dot {
278
+ width: 8px;
279
+ height: 8px;
280
+ border-radius: 50%;
281
+ display: inline-block;
282
+ flex: none;
283
+ }
284
+ .dcpa-dot-connected { background: var(--dsw-alias-state-success-primary); }
285
+ .dcpa-dot-probing { background: var(--dsw-alias-brand-primary); }
286
+ .dcpa-dot-error { background: var(--dsw-alias-state-error-primary); }
287
+ .dcpa-dot-idle { background: var(--dsw-alias-state-idle-primary); }
288
+ .dcpa-meta-item {
289
+ font-size: 12px;
290
+ color: var(--dsw-alias-label-secondary);
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 4px;
294
+ }
295
+ .dcpa-meta-val {
296
+ color: var(--dsw-alias-label-primary);
297
+ font-weight: 500;
298
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
299
+ }
300
+ .dcpa-section-title {
301
+ margin: 0;
302
+ font-size: 13px;
303
+ font-weight: 600;
304
+ color: var(--dsw-alias-label-primary);
305
+ }
306
+ .dcpa-grid {
307
+ display: grid;
308
+ grid-template-columns: 1fr 1fr;
309
+ gap: 12px;
310
+ }
311
+ @media (max-width: 560px) {
312
+ .dcpa-grid { grid-template-columns: 1fr; }
313
+ }
314
+ .dcpa-field {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 5px;
318
+ }
319
+ .dcpa-label-row {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ gap: 8px;
324
+ }
325
+ .dcpa-label {
326
+ font-size: 12px;
327
+ font-weight: 500;
328
+ color: var(--dsw-alias-label-primary);
329
+ }
330
+ .dcpa-hint {
331
+ margin: 0;
332
+ font-size: 11px;
333
+ line-height: 16px;
334
+ color: var(--dsw-alias-label-secondary);
335
+ }
336
+ .dcpa-input, .dcpa-select {
337
+ width: 100%;
338
+ box-sizing: border-box;
339
+ height: 34px;
340
+ padding: 0 10px;
341
+ font: inherit;
342
+ font-size: 13px;
343
+ color: var(--dsw-alias-label-primary);
344
+ background: var(--dsw-alias-bg-base);
345
+ border: 0.5px solid var(--dsw-alias-border-l2);
346
+ border-radius: 8px;
347
+ outline: none;
348
+ }
349
+ .dcpa-input:focus, .dcpa-select:focus {
350
+ border-color: var(--dsw-alias-brand-primary);
351
+ }
352
+ .dcpa-switch-row {
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: space-between;
356
+ gap: 12px;
357
+ }
358
+ .dcpa-switch {
359
+ position: relative;
360
+ width: 36px;
361
+ height: 20px;
362
+ border-radius: 999px;
363
+ border: none;
364
+ cursor: pointer;
365
+ background: var(--dsw-alias-state-idle-primary);
366
+ padding: 2px;
367
+ transition: background 0.15s ease;
368
+ flex: none;
369
+ }
370
+ .dcpa-switch[aria-checked="true"] {
371
+ background: var(--dsw-alias-brand-primary);
372
+ }
373
+ .dcpa-switch-thumb {
374
+ display: block;
375
+ width: 16px;
376
+ height: 16px;
377
+ border-radius: 50%;
378
+ background: var(--dsw-alias-bg-base);
379
+ transition: transform 0.15s ease;
380
+ }
381
+ .dcpa-switch[aria-checked="true"] .dcpa-switch-thumb {
382
+ transform: translateX(16px);
383
+ }
384
+ .dcpa-actions {
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 8px;
388
+ flex-wrap: wrap;
389
+ }
390
+ .dcpa-btn {
391
+ height: 30px;
392
+ padding: 0 11px;
393
+ border-radius: 8px;
394
+ font: inherit;
395
+ font-size: 12px;
396
+ font-weight: 500;
397
+ cursor: pointer;
398
+ border: 0.5px solid var(--dsw-alias-border-l2);
399
+ background: var(--dsw-alias-bg-layer-2);
400
+ color: var(--dsw-alias-label-primary);
401
+ display: inline-flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ gap: 6px;
405
+ }
406
+ .dcpa-btn:hover:not(:disabled) {
407
+ border-color: var(--dsw-alias-brand-primary);
408
+ }
409
+ .dcpa-btn:disabled {
410
+ opacity: 0.5;
411
+ cursor: default;
412
+ }
413
+ .dcpa-btn-primary {
414
+ background: var(--dsw-alias-brand-primary);
415
+ border-color: transparent;
416
+ color: var(--dsw-alias-bg-base);
417
+ }
418
+ .dcpa-btn-sm {
419
+ height: 26px;
420
+ padding: 0 9px;
421
+ font-size: 11px;
422
+ border-radius: 6px;
423
+ }
424
+ .dcpa-seg {
425
+ display: inline-flex;
426
+ border-radius: 8px;
427
+ padding: 2px;
428
+ background: var(--dsw-alias-bg-layer-2);
429
+ border: 0.5px solid var(--dsw-alias-border-l1);
430
+ gap: 2px;
431
+ }
432
+ .dcpa-seg-btn {
433
+ border: none;
434
+ background: transparent;
435
+ color: var(--dsw-alias-label-secondary);
436
+ font: inherit;
437
+ font-size: 11px;
438
+ font-weight: 500;
439
+ padding: 3px 9px;
440
+ border-radius: 6px;
441
+ cursor: pointer;
442
+ }
443
+ .dcpa-seg-btn[data-active="true"] {
444
+ background: var(--dsw-alias-bg-base);
445
+ color: var(--dsw-alias-label-primary);
446
+ box-shadow: 0 1px 2px rgba(0,0,0,0.08);
447
+ }
448
+ .dcpa-toolbar {
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: space-between;
452
+ gap: 8px;
453
+ flex-wrap: wrap;
454
+ }
455
+ .dcpa-family-chips {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 6px;
459
+ flex-wrap: wrap;
460
+ }
461
+ .dcpa-chip {
462
+ border: 0.5px solid var(--dsw-alias-border-l2);
463
+ background: var(--dsw-alias-bg-layer-2);
464
+ color: var(--dsw-alias-label-secondary);
465
+ border-radius: 999px;
466
+ padding: 2px 9px;
467
+ font: inherit;
468
+ font-size: 11px;
469
+ cursor: pointer;
470
+ }
471
+ .dcpa-chip[data-active="true"] {
472
+ border-color: var(--dsw-alias-brand-primary);
473
+ color: var(--dsw-alias-brand-primary);
474
+ font-weight: 600;
475
+ }
476
+ .dcpa-badge {
477
+ font-size: 11px;
478
+ padding: 1px 6px;
479
+ border-radius: 6px;
480
+ border: 0.5px solid var(--dsw-alias-border-l2);
481
+ background: var(--dsw-alias-bg-layer-2);
482
+ color: var(--dsw-alias-label-secondary);
483
+ }
484
+ .dcpa-badge-ok {
485
+ color: var(--dsw-alias-state-success-primary);
486
+ border-color: var(--dsw-alias-state-success-primary);
487
+ }
488
+ .dcpa-badge-warn {
489
+ color: var(--dsw-alias-state-warn-primary);
490
+ border-color: var(--dsw-alias-state-warn-primary);
491
+ }
492
+ .dcpa-error {
493
+ margin: 0;
494
+ padding: 8px 10px;
495
+ border-radius: 8px;
496
+ border: 0.5px solid var(--dsw-alias-state-error-primary);
497
+ color: var(--dsw-alias-state-error-primary);
498
+ font-size: 12px;
499
+ }
500
+ .dcpa-success {
501
+ margin: 0;
502
+ font-size: 12px;
503
+ color: var(--dsw-alias-state-success-primary);
504
+ }
505
+ .dcpa-model-list {
506
+ max-height: 360px;
507
+ overflow-y: auto;
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 6px;
511
+ padding-right: 2px;
512
+ }
513
+ .dcpa-model-row {
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: space-between;
517
+ gap: 10px;
518
+ padding: 8px 11px;
519
+ border-radius: 8px;
520
+ background: var(--dsw-alias-bg-layer-2);
521
+ border: 0.5px solid var(--dsw-alias-border-l1);
522
+ cursor: pointer;
523
+ user-select: none;
524
+ flex-wrap: wrap;
525
+ transition: border-color 0.12s ease;
526
+ }
527
+ .dcpa-model-row:hover {
528
+ border-color: var(--dsw-alias-border-l2);
529
+ }
530
+ .dcpa-model-row[data-checked="true"] {
531
+ border-color: var(--dsw-alias-brand-primary);
532
+ background: var(--dsw-alias-bg-base);
533
+ }
534
+ .dcpa-model-left {
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 9px;
538
+ min-width: 0;
539
+ }
540
+ .dcpa-checkbox {
541
+ width: 16px;
542
+ height: 16px;
543
+ border-radius: 4px;
544
+ border: 1px solid var(--dsw-alias-border-l2);
545
+ background: var(--dsw-alias-bg-base);
546
+ display: inline-flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ flex: none;
550
+ color: var(--dsw-alias-bg-base);
551
+ font-size: 11px;
552
+ font-weight: 700;
553
+ }
554
+ .dcpa-model-row[data-checked="true"] .dcpa-checkbox {
555
+ background: var(--dsw-alias-brand-primary);
556
+ border-color: var(--dsw-alias-brand-primary);
557
+ }
558
+ .dcpa-model-id {
559
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
560
+ font-size: 12px;
561
+ font-weight: 500;
562
+ color: var(--dsw-alias-label-primary);
563
+ }
564
+ .dcpa-model-name {
565
+ font-size: 12px;
566
+ color: var(--dsw-alias-label-secondary);
567
+ margin-left: 6px;
568
+ }
569
+ .dcpa-model-tags {
570
+ display: inline-flex;
571
+ align-items: center;
572
+ gap: 6px;
573
+ flex-wrap: wrap;
574
+ }
575
+ .dcpa-model-main {
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: space-between;
579
+ gap: 10px;
580
+ width: 100%;
581
+ }
582
+ .dcpa-edit-btn {
583
+ height: 24px;
584
+ padding: 0 8px;
585
+ border-radius: 6px;
586
+ border: 0.5px solid var(--dsw-alias-border-l2);
587
+ background: var(--dsw-alias-bg-layer-2);
588
+ color: var(--dsw-alias-label-primary);
589
+ font: inherit;
590
+ font-size: 11px;
591
+ font-weight: 500;
592
+ cursor: pointer;
593
+ }
594
+ .dcpa-edit-btn:hover, .dcpa-edit-btn[data-active="true"] {
595
+ border-color: var(--dsw-alias-brand-primary);
596
+ color: var(--dsw-alias-brand-primary);
597
+ }
598
+ .dcpa-editor {
599
+ width: 100%;
600
+ display: flex;
601
+ flex-direction: column;
602
+ gap: 10px;
603
+ padding-top: 8px;
604
+ border-top: 0.5px solid var(--dsw-alias-border-l1);
605
+ }
606
+ .dcpa-level-list {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 6px;
610
+ flex-wrap: wrap;
611
+ }
612
+ .dcpa-level {
613
+ display: inline-flex;
614
+ align-items: center;
615
+ gap: 4px;
616
+ font-size: 11px;
617
+ color: var(--dsw-alias-label-secondary);
618
+ }
619
+ .dcpa-account-bar {
620
+ display: flex;
621
+ align-items: center;
622
+ gap: 6px;
623
+ flex-wrap: wrap;
624
+ }
625
+ .dcpa-account-chip {
626
+ display: inline-flex;
627
+ align-items: center;
628
+ gap: 6px;
629
+ height: 28px;
630
+ padding: 0 10px;
631
+ border-radius: 8px;
632
+ border: 0.5px solid var(--dsw-alias-border-l2);
633
+ background: var(--dsw-alias-bg-layer-2);
634
+ color: var(--dsw-alias-label-primary);
635
+ font: inherit;
636
+ font-size: 12px;
637
+ cursor: pointer;
638
+ }
639
+ .dcpa-account-chip[data-active="true"] {
640
+ border-color: var(--dsw-alias-brand-primary);
641
+ background: var(--dsw-alias-bg-base);
642
+ font-weight: 600;
643
+ }
644
+ .dcpa-account-chip[data-enabled="false"] {
645
+ color: var(--dsw-alias-label-secondary);
646
+ }
647
+ .dcpa-btn-danger {
648
+ color: var(--dsw-alias-state-error-primary);
649
+ }
650
+ .dcpa-btn-danger:hover:not(:disabled) {
651
+ border-color: var(--dsw-alias-state-error-primary);
652
+ }
653
+ `;
654
+
655
+ const REASONING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
656
+ const PROTOCOL_NAME_KEYS = {
657
+ "openai-completions": "protocolNameCompletions",
658
+ "openai-responses": "protocolNameResponses",
659
+ "anthropic-messages": "protocolNameMessages",
660
+ };
661
+ const PROTOCOLS = Object.keys(PROTOCOL_NAME_KEYS);
662
+
663
+ function cloneOverrides(value) {
664
+ const out = {};
665
+ for (const [id, entry] of Object.entries(value ?? {})) {
666
+ out[id] = {
667
+ ...entry,
668
+ ...(Array.isArray(entry.reasoningLevels) ? { reasoningLevels: [...entry.reasoningLevels] } : {}),
669
+ };
670
+ }
671
+ return out;
672
+ }
673
+
674
+ function overridesKey(value) {
675
+ return JSON.stringify(value ?? {});
676
+ }
677
+
678
+ function formatTokens(n) {
679
+ if (typeof n !== "number" || !Number.isFinite(n)) return "";
680
+ if (n >= 1000000) return `${(n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1)}M`;
681
+ if (n >= 1000) return `${Math.round(n / 1000)}K`;
682
+ return String(n);
683
+ }
684
+
685
+ async function postJson(url, body) {
686
+ const res = await fetch(url, {
687
+ method: "POST",
688
+ headers: { "content-type": "application/json" },
689
+ body: JSON.stringify(body ?? {}),
690
+ });
691
+ const payload = await res.json();
692
+ if (!res.ok || payload.ok === false) {
693
+ throw new Error(payload.error || `HTTP ${res.status}`);
694
+ }
695
+ return payload;
696
+ }
697
+
698
+ function stateDotClass(state) {
699
+ return state === "connected"
700
+ ? "dcpa-dot dcpa-dot-connected"
701
+ : state === "probing"
702
+ ? "dcpa-dot dcpa-dot-probing"
703
+ : state === "error"
704
+ ? "dcpa-dot dcpa-dot-error"
705
+ : "dcpa-dot dcpa-dot-idle";
706
+ }
707
+
708
+ /**
709
+ * Account list + switcher. Each account is edited in its own
710
+ * {@link AccountPanel}, remounted when the selection changes.
711
+ */
712
+ function CpaSettingsPanel({ t }) {
713
+ const [loading, setLoading] = useState(true);
714
+ const [busy, setBusy] = useState(null); // null | "refresh" | "save" | "add" | "remove"
715
+ const [error, setError] = useState(null);
716
+ const [toast, setToast] = useState(null);
717
+ const [data, setData] = useState(null);
718
+ const [activeId, setActiveId] = useState(null);
719
+ // Bumped whenever the server's saved config should replace the drafts.
720
+ const [rev, setRev] = useState(0);
721
+
722
+ const accounts = Array.isArray(data?.accounts) ? data.accounts : [];
723
+ const active = accounts.find((a) => a.id === activeId) ?? accounts[0];
724
+
725
+ const fetchStatus = useCallback(async () => {
726
+ try {
727
+ const res = await fetch(API.status, { cache: "no-store" });
728
+ const body = await res.json();
729
+ if (!res.ok || body.ok === false) {
730
+ throw new Error(body.error || `HTTP ${res.status}`);
731
+ }
732
+ setData(body);
733
+ setRev((r) => r + 1);
734
+ setError(null);
735
+ } catch (err) {
736
+ setError(err instanceof Error ? err.message : String(err));
737
+ } finally {
738
+ setLoading(false);
739
+ }
740
+ }, []);
741
+
742
+ useEffect(() => {
743
+ void fetchStatus();
744
+ }, [fetchStatus]);
745
+
746
+ const run = useCallback(async (kind, fn) => {
747
+ setBusy(kind);
748
+ setError(null);
749
+ setToast(null);
750
+ try {
751
+ return await fn();
752
+ } catch (err) {
753
+ setError(err instanceof Error ? err.message : String(err));
754
+ return undefined;
755
+ } finally {
756
+ setBusy(null);
757
+ }
758
+ }, []);
759
+
760
+ const onRefresh = useCallback(
761
+ (accountId) =>
762
+ run("refresh", async () => {
763
+ setData(await postJson(API.refresh, { accountId }));
764
+ }),
765
+ [run]
766
+ );
767
+
768
+ const onSave = useCallback(
769
+ (accountId, payload) =>
770
+ run("save", async () => {
771
+ setData(await postJson(API.save, { ...payload, accountId }));
772
+ setRev((r) => r + 1);
773
+ setToast(t("savedToast"));
774
+ }),
775
+ [run, t]
776
+ );
777
+
778
+ const onAdd = useCallback(
779
+ () =>
780
+ run("add", async () => {
781
+ const body = await postJson(API.addAccount, {});
782
+ setData(body);
783
+ if (body.createdId) setActiveId(body.createdId);
784
+ setRev((r) => r + 1);
785
+ setToast(t("addedToast"));
786
+ }),
787
+ [run, t]
788
+ );
789
+
790
+ const onRemove = useCallback(
791
+ (account) => {
792
+ const name = account.config?.displayName || account.id;
793
+ if (typeof window !== "undefined" && !window.confirm(t("removeConfirm").replace("{name}", name))) {
794
+ return;
795
+ }
796
+ void run("remove", async () => {
797
+ const body = await postJson(API.removeAccount, { accountId: account.id });
798
+ setData(body);
799
+ setActiveId(body.accounts?.[0]?.id ?? null);
800
+ setRev((r) => r + 1);
801
+ setToast(t("removedToast"));
802
+ });
803
+ },
804
+ [run, t]
805
+ );
806
+
807
+ return h(
808
+ "div",
809
+ { className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
810
+ h(
811
+ "div",
812
+ { className: "dcpa-card" },
813
+ h(
814
+ "div",
815
+ { className: "dcpa-head" },
816
+ h(
817
+ "div",
818
+ null,
819
+ h("h3", { className: "dcpa-title" }, t("title")),
820
+ h("p", { className: "dcpa-subtitle" }, t("subtitle"))
821
+ )
822
+ ),
823
+ h(
824
+ "div",
825
+ { className: "dcpa-account-bar", role: "tablist", "aria-label": t("accountsLabel") },
826
+ accounts.map((a) =>
827
+ h(
828
+ "button",
829
+ {
830
+ key: a.id,
831
+ type: "button",
832
+ role: "tab",
833
+ className: "dcpa-account-chip",
834
+ "aria-selected": active?.id === a.id,
835
+ "data-active": active?.id === a.id ? "true" : "false",
836
+ "data-enabled": a.config?.enabled === false ? "false" : "true",
837
+ title: a.config?.provider,
838
+ onClick: () => setActiveId(a.id),
839
+ },
840
+ h("span", { className: stateDotClass(a.status?.state) }),
841
+ a.config?.displayName || a.id,
842
+ a.config?.enabled === false
843
+ ? h("span", { className: "dcpa-badge" }, t("accountOffBadge"))
844
+ : null
845
+ )
846
+ ),
847
+ h(
848
+ "button",
849
+ {
850
+ type: "button",
851
+ className: "dcpa-btn dcpa-btn-sm",
852
+ disabled: busy !== null || loading,
853
+ onClick: onAdd,
854
+ },
855
+ busy === "add" ? t("addingAccountBtn") : t("addAccountBtn")
856
+ )
857
+ ),
858
+ !loading && accounts.length === 0 ? h("p", { className: "dcpa-hint" }, t("noAccounts")) : null,
859
+ error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
860
+ toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
861
+ ),
862
+ active
863
+ ? h(AccountPanel, {
864
+ key: active.id,
865
+ t,
866
+ account: active,
867
+ global: data?.global ?? {},
868
+ rev,
869
+ busy,
870
+ onRefresh: () => onRefresh(active.id),
871
+ onSave: (payload) => onSave(active.id, payload),
872
+ onRemove: () => onRemove(active),
873
+ })
874
+ : null
875
+ );
876
+ }
877
+
878
+ function AccountPanel({ t, account, global, rev, busy, onRefresh, onSave: saveAccount, onRemove }) {
879
+ const syncing = busy === "refresh";
880
+ const saving = busy === "save";
881
+ const loading = busy === "add" || busy === "remove";
882
+
883
+ // Model filter UI state
884
+ const [search, setSearch] = useState("");
885
+ const [tabFilter, setTabFilter] = useState("all"); // "all" | "selected" | "unselected"
886
+ const [familyFilter, setFamilyFilter] = useState("all");
887
+ const [selectedIds, setSelectedIds] = useState(() => new Set());
888
+ const [savedIdsKey, setSavedIdsKey] = useState("");
889
+ const [modelOverrides, setModelOverrides] = useState({});
890
+ const [savedOverridesKey, setSavedOverridesKey] = useState("{}");
891
+ const [editingId, setEditingId] = useState(null);
892
+
893
+ // Form draft state
894
+ const [enabled, setEnabled] = useState(true);
895
+ const [baseURL, setBaseURL] = useState("");
896
+ const [apiKeyDraft, setApiKeyDraft] = useState("");
897
+ // "auto" or the protocol every model is fixed to.
898
+ const [protocol, setProtocol] = useState("auto");
899
+ const [provider, setProvider] = useState("cli-proxy");
900
+ const [displayName, setDisplayName] = useState("CLI Proxy API");
901
+ const [apiKeyEnv, setApiKeyEnv] = useState("CLI_PROXY_API_KEY");
902
+ const [includePrefix, setIncludePrefix] = useState("");
903
+ const [excludePrefix, setExcludePrefix] = useState("");
904
+ const [refreshIntervalSeconds, setRefreshIntervalSeconds] = useState("300");
905
+ const [probeTimeoutMs, setProbeTimeoutMs] = useState("5000");
906
+
907
+ // Load the drafts from the saved config on mount and after every save.
908
+ useEffect(() => {
909
+ const c = account.config ?? {};
910
+ setEnabled(c.enabled !== false);
911
+ setBaseURL(c.baseURL ?? "");
912
+ setProtocol(c.protocolMode === "fixed" ? c.protocol ?? "openai-completions" : "auto");
913
+ setProvider(c.provider ?? "cli-proxy");
914
+ setDisplayName(c.displayName ?? "CLI Proxy API");
915
+ setApiKeyEnv(c.effectiveApiKeyEnv ?? c.apiKeyEnv ?? "CLI_PROXY_API_KEY");
916
+ setIncludePrefix(c.includePrefix ?? "");
917
+ setExcludePrefix(c.excludePrefix ?? "");
918
+ setRefreshIntervalSeconds(String(global?.refreshIntervalSeconds ?? 300));
919
+ setProbeTimeoutMs(String(global?.probeTimeoutMs ?? 5000));
920
+ setApiKeyDraft("");
921
+
922
+ const sel = Array.isArray(c.selectedModels)
923
+ ? c.selectedModels
924
+ : Array.isArray(account.status?.models)
925
+ ? account.status.models.map((m) => m.id)
926
+ : [];
927
+ const nextSet = new Set(sel);
928
+ setSelectedIds(nextSet);
929
+ setSavedIdsKey([...nextSet].sort().join("\0"));
930
+ const nextOverrides = cloneOverrides(c.modelOverrides);
931
+ setModelOverrides(nextOverrides);
932
+ setSavedOverridesKey(overridesKey(nextOverrides));
933
+ // eslint-disable-next-line react-hooks/exhaustive-deps
934
+ }, [rev]);
935
+
936
+ const onRefreshNow = onRefresh;
937
+
938
+ const onSave = useCallback(
939
+ (clearKey = false) =>
940
+ saveAccount({
941
+ enabled,
942
+ baseURL: baseURL.trim(),
943
+ protocolMode: protocol === "auto" ? "auto" : "fixed",
944
+ ...(protocol === "auto" ? {} : { protocol }),
945
+ provider: provider.trim() || account.config?.provider || "cli-proxy",
946
+ displayName: displayName.trim() || account.config?.displayName || "CLI Proxy API",
947
+ apiKeyEnv: apiKeyEnv.trim() || account.config?.apiKeyEnv || "CLI_PROXY_API_KEY",
948
+ includePrefix: includePrefix.trim(),
949
+ excludePrefix: excludePrefix.trim(),
950
+ // Before the first catalog is seen, an empty draft is not a choice:
951
+ // leave the selection unset so the first probe can recommend one.
952
+ ...(selectedIds.size > 0 || (account.status?.allModels?.length ?? 0) > 0
953
+ ? { selectedModels: [...selectedIds] }
954
+ : {}),
955
+ modelOverrides,
956
+ refreshIntervalSeconds: Math.max(15, Number(refreshIntervalSeconds) || 300),
957
+ probeTimeoutMs: Math.max(250, Number(probeTimeoutMs) || 5000),
958
+ ...(apiKeyDraft.trim() !== "" ? { apiKey: apiKeyDraft.trim() } : {}),
959
+ ...(clearKey ? { clearApiKey: true } : {}),
960
+ }),
961
+ [
962
+ saveAccount,
963
+ account,
964
+ selectedIds,
965
+ modelOverrides,
966
+ enabled,
967
+ baseURL,
968
+ protocol,
969
+ provider,
970
+ displayName,
971
+ apiKeyEnv,
972
+ includePrefix,
973
+ excludePrefix,
974
+ refreshIntervalSeconds,
975
+ probeTimeoutMs,
976
+ apiKeyDraft,
977
+ ]
978
+ );
979
+
980
+ const st = account.status ?? { state: "idle", modelCount: 0, models: [], allModels: [] };
981
+ const cred = account.credential ?? { configured: false, ref: apiKeyEnv };
982
+ const effectiveUrl = account.config?.effectiveBaseURL || "";
983
+
984
+ const allModels = useMemo(() => {
985
+ if (Array.isArray(st.allModels) && st.allModels.length > 0) return st.allModels;
986
+ if (Array.isArray(st.models)) return st.models;
987
+ return [];
988
+ }, [st.allModels, st.models]);
989
+
990
+ const families = useMemo(() => {
991
+ const counts = new Map();
992
+ for (const m of allModels) {
993
+ const fam = m.family || "other";
994
+ counts.set(fam, (counts.get(fam) ?? 0) + 1);
995
+ }
996
+ return [...counts.entries()];
997
+ }, [allModels]);
998
+
999
+ const visibleModels = useMemo(() => {
1000
+ const q = search.trim().toLowerCase();
1001
+ return allModels.filter((m) => {
1002
+ const isChecked = selectedIds.has(m.id);
1003
+ if (tabFilter === "selected" && !isChecked) return false;
1004
+ if (tabFilter === "unselected" && isChecked) return false;
1005
+ if (familyFilter !== "all" && (m.family || "other") !== familyFilter) return false;
1006
+ if (q) {
1007
+ const matchId = String(m.id).toLowerCase().includes(q);
1008
+ const matchName = String(m.name ?? "").toLowerCase().includes(q);
1009
+ if (!matchId && !matchName) return false;
1010
+ }
1011
+ return true;
1012
+ });
1013
+ }, [allModels, search, tabFilter, familyFilter, selectedIds]);
1014
+
1015
+ const currentIdsKey = useMemo(() => [...selectedIds].sort().join("\0"), [selectedIds]);
1016
+ const currentOverridesKey = useMemo(() => overridesKey(modelOverrides), [modelOverrides]);
1017
+ const selectionDirty =
1018
+ (savedIdsKey !== "" && currentIdsKey !== savedIdsKey) ||
1019
+ currentOverridesKey !== savedOverridesKey;
1020
+
1021
+ const toggleModel = useCallback((id) => {
1022
+ setSelectedIds((prev) => {
1023
+ const next = new Set(prev);
1024
+ if (next.has(id)) next.delete(id);
1025
+ else next.add(id);
1026
+ return next;
1027
+ });
1028
+ }, []);
1029
+
1030
+ const checkVisible = useCallback(() => {
1031
+ setSelectedIds((prev) => {
1032
+ const next = new Set(prev);
1033
+ for (const m of visibleModels) next.add(m.id);
1034
+ return next;
1035
+ });
1036
+ }, [visibleModels]);
1037
+
1038
+ const uncheckVisible = useCallback(() => {
1039
+ setSelectedIds((prev) => {
1040
+ const next = new Set(prev);
1041
+ for (const m of visibleModels) next.delete(m.id);
1042
+ return next;
1043
+ });
1044
+ }, [visibleModels]);
1045
+
1046
+ // Computed by the Host from this account's own catalog: newest per family.
1047
+ const recommended = Array.isArray(account.recommendedModels) ? account.recommendedModels : [];
1048
+ const resetRecommended = useCallback(() => {
1049
+ setSelectedIds(new Set(recommended));
1050
+ }, [recommended]);
1051
+
1052
+ const updateOverride = useCallback((model, patch) => {
1053
+ setModelOverrides((prev) => {
1054
+ const next = cloneOverrides(prev);
1055
+ const discovered = model.discovered ?? {};
1056
+ const current = {
1057
+ contextWindow: discovered.contextWindow,
1058
+ maxTokens: discovered.maxTokens,
1059
+ image: discovered.image === true,
1060
+ reasoningLevels: Array.isArray(discovered.reasoningLevels) ? [...discovered.reasoningLevels] : [],
1061
+ ...(next[model.id] ?? {}),
1062
+ };
1063
+ const merged = { ...current, ...patch };
1064
+ const normalized = {};
1065
+ if (
1066
+ Number.isInteger(merged.contextWindow) &&
1067
+ merged.contextWindow > 0 &&
1068
+ merged.contextWindow !== discovered.contextWindow
1069
+ ) {
1070
+ normalized.contextWindow = merged.contextWindow;
1071
+ }
1072
+ if (
1073
+ Number.isInteger(merged.maxTokens) &&
1074
+ merged.maxTokens > 0 &&
1075
+ merged.maxTokens !== discovered.maxTokens
1076
+ ) {
1077
+ normalized.maxTokens = merged.maxTokens;
1078
+ }
1079
+ if (merged.image !== discovered.image) normalized.image = merged.image === true;
1080
+ const discoveredLevels = Array.isArray(discovered.reasoningLevels) ? discovered.reasoningLevels : [];
1081
+ const levels = Array.isArray(merged.reasoningLevels) ? merged.reasoningLevels : [];
1082
+ if (levels.join("\0") !== discoveredLevels.join("\0")) normalized.reasoningLevels = levels;
1083
+ if (PROTOCOLS.includes(merged.protocol)) normalized.protocol = merged.protocol;
1084
+ if (Object.keys(normalized).length === 0) delete next[model.id];
1085
+ else next[model.id] = normalized;
1086
+ return next;
1087
+ });
1088
+ }, []);
1089
+
1090
+ // The protocol a model would use with the panel's current draft settings.
1091
+ const effectiveProtocol = useCallback(
1092
+ (m) =>
1093
+ modelOverrides[m.id]?.protocol ??
1094
+ (protocol === "auto" ? m.nativeProtocol ?? "openai-completions" : protocol),
1095
+ [modelOverrides, protocol]
1096
+ );
1097
+ const protocolName = (p) => t(PROTOCOL_NAME_KEYS[p] ?? "protocolNameCompletions");
1098
+
1099
+ const resetOverride = useCallback((id) => {
1100
+ setModelOverrides((prev) => {
1101
+ const next = cloneOverrides(prev);
1102
+ delete next[id];
1103
+ return next;
1104
+ });
1105
+ }, []);
1106
+
1107
+ const dotClass = stateDotClass(st.state);
1108
+
1109
+ const stateLabel =
1110
+ st.state === "connected"
1111
+ ? t("statusConnected")
1112
+ : st.state === "probing"
1113
+ ? t("statusProbing")
1114
+ : st.state === "error"
1115
+ ? t("statusError")
1116
+ : st.state === "disabled"
1117
+ ? t("statusDisabled")
1118
+ : t("statusIdle");
1119
+
1120
+ return h(
1121
+ React.Fragment,
1122
+ null,
1123
+ // 1. Account Status Card
1124
+ h(
1125
+ "div",
1126
+ { className: "dcpa-card" },
1127
+ h(
1128
+ "div",
1129
+ { className: "dcpa-head" },
1130
+ h(
1131
+ "div",
1132
+ null,
1133
+ h("h4", { className: "dcpa-section-title" }, account.config?.displayName || account.id),
1134
+ h(
1135
+ "p",
1136
+ { className: "dcpa-hint" },
1137
+ `${t("providerIdLabel")}: ${account.config?.provider ?? ""}`
1138
+ )
1139
+ ),
1140
+ h(
1141
+ "div",
1142
+ { className: "dcpa-actions" },
1143
+ h(
1144
+ "button",
1145
+ {
1146
+ type: "button",
1147
+ className: "dcpa-btn",
1148
+ disabled: syncing || saving || loading,
1149
+ onClick: onRefreshNow,
1150
+ },
1151
+ syncing ? t("refreshingBtn") : t("refreshBtn")
1152
+ ),
1153
+ h(
1154
+ "button",
1155
+ {
1156
+ type: "button",
1157
+ className: "dcpa-btn dcpa-btn-primary",
1158
+ disabled: saving || syncing || loading,
1159
+ onClick: () => onSave(false),
1160
+ },
1161
+ saving ? t("savingBtn") : t("saveBtn")
1162
+ )
1163
+ )
1164
+ ),
1165
+ h(
1166
+ "div",
1167
+ { className: "dcpa-status-bar" },
1168
+ h(
1169
+ "span",
1170
+ { className: "dcpa-status-pill" },
1171
+ h("span", { className: dotClass }),
1172
+ stateLabel
1173
+ ),
1174
+ h(
1175
+ "span",
1176
+ { className: "dcpa-meta-item" },
1177
+ `${t("endpointLabel")}:`,
1178
+ h(
1179
+ "span",
1180
+ { className: "dcpa-meta-val" },
1181
+ st.endpoint || effectiveUrl || t("endpointNone")
1182
+ )
1183
+ ),
1184
+ h(
1185
+ "span",
1186
+ { className: "dcpa-meta-item" },
1187
+ `${t("modelsCountLabel")}:`,
1188
+ h(
1189
+ "span",
1190
+ { className: "dcpa-meta-val" },
1191
+ `${st.modelCount ?? 0} / ${st.totalDiscovered ?? allModels.length}`
1192
+ )
1193
+ )
1194
+ ),
1195
+ account.config?.enabled === false ? h("p", { className: "dcpa-hint" }, t("disabledHint")) : null,
1196
+ st.error ? h("p", { className: "dcpa-error", role: "alert" }, st.error) : null
1197
+ ),
1198
+
1199
+ // 2. Interactive Model Selection Filter Card
1200
+ h(
1201
+ "div",
1202
+ { className: "dcpa-card" },
1203
+ h(
1204
+ "div",
1205
+ { className: "dcpa-head" },
1206
+ h(
1207
+ "div",
1208
+ null,
1209
+ h(
1210
+ "h4",
1211
+ { className: "dcpa-section-title" },
1212
+ `${t("sectionModels")} (${selectedIds.size} / ${allModels.length})`
1213
+ ),
1214
+ h("p", { className: "dcpa-hint" }, t("sectionModelsHint"))
1215
+ ),
1216
+ h(
1217
+ "div",
1218
+ { className: "dcpa-actions" },
1219
+ selectionDirty
1220
+ ? h("span", { className: "dcpa-badge dcpa-badge-warn" }, t("unsavedChanges"))
1221
+ : null,
1222
+ h(
1223
+ "button",
1224
+ {
1225
+ type: "button",
1226
+ className: "dcpa-btn dcpa-btn-primary dcpa-btn-sm",
1227
+ disabled: saving || syncing,
1228
+ onClick: () => onSave(false),
1229
+ },
1230
+ saving ? t("savingBtn") : t("saveSelectionBtn")
1231
+ )
1232
+ )
1233
+ ),
1234
+
1235
+ // Search + Segmented Filter + Batch Actions
1236
+ h(
1237
+ "div",
1238
+ { className: "dcpa-toolbar" },
1239
+ h(
1240
+ "div",
1241
+ { className: "dcpa-seg" },
1242
+ h(
1243
+ "button",
1244
+ {
1245
+ type: "button",
1246
+ className: "dcpa-seg-btn",
1247
+ "data-active": tabFilter === "all" ? "true" : "false",
1248
+ onClick: () => setTabFilter("all"),
1249
+ },
1250
+ `${t("filterAll")} (${allModels.length})`
1251
+ ),
1252
+ h(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ className: "dcpa-seg-btn",
1257
+ "data-active": tabFilter === "selected" ? "true" : "false",
1258
+ onClick: () => setTabFilter("selected"),
1259
+ },
1260
+ `${t("filterSelected")} (${selectedIds.size})`
1261
+ ),
1262
+ h(
1263
+ "button",
1264
+ {
1265
+ type: "button",
1266
+ className: "dcpa-seg-btn",
1267
+ "data-active": tabFilter === "unselected" ? "true" : "false",
1268
+ onClick: () => setTabFilter("unselected"),
1269
+ },
1270
+ `${t("filterUnselected")} (${Math.max(0, allModels.length - selectedIds.size)})`
1271
+ )
1272
+ ),
1273
+ h(
1274
+ "div",
1275
+ { className: "dcpa-actions" },
1276
+ h(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ className: "dcpa-btn dcpa-btn-sm",
1281
+ onClick: checkVisible,
1282
+ },
1283
+ t("selectAllVisibleBtn")
1284
+ ),
1285
+ h(
1286
+ "button",
1287
+ {
1288
+ type: "button",
1289
+ className: "dcpa-btn dcpa-btn-sm",
1290
+ onClick: uncheckVisible,
1291
+ },
1292
+ t("deselectAllVisibleBtn")
1293
+ ),
1294
+ h(
1295
+ "button",
1296
+ {
1297
+ type: "button",
1298
+ className: "dcpa-btn dcpa-btn-sm",
1299
+ disabled: recommended.length === 0,
1300
+ title: t("selectRecommendedHint"),
1301
+ onClick: resetRecommended,
1302
+ },
1303
+ `${t("selectRecommendedBtn")} (${recommended.length})`
1304
+ )
1305
+ )
1306
+ ),
1307
+
1308
+ // Family Quick Chips
1309
+ families.length > 1
1310
+ ? h(
1311
+ "div",
1312
+ { className: "dcpa-family-chips" },
1313
+ h(
1314
+ "button",
1315
+ {
1316
+ type: "button",
1317
+ className: "dcpa-chip",
1318
+ "data-active": familyFilter === "all" ? "true" : "false",
1319
+ onClick: () => setFamilyFilter("all"),
1320
+ },
1321
+ t("familyAll")
1322
+ ),
1323
+ families.map(([fam, count]) =>
1324
+ h(
1325
+ "button",
1326
+ {
1327
+ key: fam,
1328
+ type: "button",
1329
+ className: "dcpa-chip",
1330
+ "data-active": familyFilter === fam ? "true" : "false",
1331
+ onClick: () => setFamilyFilter(familyFilter === fam ? "all" : fam),
1332
+ },
1333
+ `${fam.toUpperCase()} (${count})`
1334
+ )
1335
+ )
1336
+ )
1337
+ : null,
1338
+
1339
+ h("input", {
1340
+ type: "text",
1341
+ className: "dcpa-input",
1342
+ value: search,
1343
+ placeholder: t("searchModelsPlaceholder"),
1344
+ onChange: (e) => setSearch(e.target.value),
1345
+ }),
1346
+
1347
+ visibleModels.length === 0
1348
+ ? h("p", { className: "dcpa-hint" }, t("noModelsText"))
1349
+ : h(
1350
+ "div",
1351
+ { className: "dcpa-model-list" },
1352
+ visibleModels.map((m) => {
1353
+ const checked = selectedIds.has(m.id);
1354
+ const editing = editingId === m.id;
1355
+ const hasOverride = modelOverrides[m.id] !== undefined;
1356
+ const discovered = m.discovered ?? {};
1357
+ const draft = {
1358
+ contextWindow: discovered.contextWindow ?? m.contextWindow,
1359
+ maxTokens: discovered.maxTokens ?? m.maxTokens,
1360
+ image: discovered.image === true,
1361
+ reasoningLevels: Array.isArray(discovered.reasoningLevels) ? discovered.reasoningLevels : [],
1362
+ ...(modelOverrides[m.id] ?? {}),
1363
+ };
1364
+ return h(
1365
+ "div",
1366
+ {
1367
+ key: m.id,
1368
+ className: "dcpa-model-row",
1369
+ "data-checked": checked ? "true" : "false",
1370
+ },
1371
+ h(
1372
+ "div",
1373
+ { className: "dcpa-model-main" },
1374
+ h(
1375
+ "div",
1376
+ {
1377
+ className: "dcpa-model-left",
1378
+ role: "checkbox",
1379
+ "aria-checked": checked,
1380
+ tabIndex: 0,
1381
+ onClick: () => toggleModel(m.id),
1382
+ onKeyDown: (e) => {
1383
+ if (e.key === " " || e.key === "Enter") {
1384
+ e.preventDefault();
1385
+ toggleModel(m.id);
1386
+ }
1387
+ },
1388
+ },
1389
+ h("span", { className: "dcpa-checkbox" }, checked ? "✓" : ""),
1390
+ h(
1391
+ "div",
1392
+ null,
1393
+ h("span", { className: "dcpa-model-id" }, m.id),
1394
+ m.name && m.name !== m.id
1395
+ ? h("span", { className: "dcpa-model-name" }, `(${m.name})`)
1396
+ : null
1397
+ )
1398
+ ),
1399
+ h(
1400
+ "div",
1401
+ { className: "dcpa-model-tags" },
1402
+ hasOverride
1403
+ ? h("span", { className: "dcpa-badge dcpa-badge-warn" }, t("customParamsBadge"))
1404
+ : null,
1405
+ checked
1406
+ ? h("span", { className: "dcpa-badge" }, protocolName(effectiveProtocol(m)))
1407
+ : null,
1408
+ m.image
1409
+ ? h("span", { className: "dcpa-badge dcpa-badge-ok" }, t("badgeVision"))
1410
+ : null,
1411
+ m.reasoning
1412
+ ? h(
1413
+ "span",
1414
+ { className: "dcpa-badge" },
1415
+ `${t("badgeReasoning")}${
1416
+ Array.isArray(m.reasoningLevels) && m.reasoningLevels.length > 0
1417
+ ? `: ${m.reasoningLevels.join("/")}`
1418
+ : ""
1419
+ }`
1420
+ )
1421
+ : null,
1422
+ m.contextWindow
1423
+ ? h(
1424
+ "span",
1425
+ { className: "dcpa-badge" },
1426
+ `${t("contextWindowTag")} ${formatTokens(m.contextWindow)}`
1427
+ )
1428
+ : null,
1429
+ checked
1430
+ ? h(
1431
+ "button",
1432
+ {
1433
+ type: "button",
1434
+ className: "dcpa-edit-btn",
1435
+ "data-active": editing ? "true" : "false",
1436
+ onClick: () => setEditingId(editing ? null : m.id),
1437
+ },
1438
+ editing ? t("editingParamsBtn") : t("editParamsBtn")
1439
+ )
1440
+ : null
1441
+ )
1442
+ ),
1443
+ checked && editing
1444
+ ? h(
1445
+ "div",
1446
+ { className: "dcpa-editor" },
1447
+ h(
1448
+ "div",
1449
+ { className: "dcpa-grid" },
1450
+ h(
1451
+ "div",
1452
+ { className: "dcpa-field" },
1453
+ h("label", { className: "dcpa-label" }, t("contextWindowLabel")),
1454
+ h("input", {
1455
+ type: "number",
1456
+ min: "1",
1457
+ className: "dcpa-input",
1458
+ value: draft.contextWindow ?? "",
1459
+ onChange: (e) => {
1460
+ const value = Number(e.target.value);
1461
+ if (Number.isInteger(value) && value > 0) {
1462
+ updateOverride(m, { contextWindow: value });
1463
+ }
1464
+ },
1465
+ }),
1466
+ h(
1467
+ "p",
1468
+ { className: "dcpa-hint" },
1469
+ `${t("discoveredValue")}: ${formatTokens(m.discovered?.contextWindow)}`
1470
+ )
1471
+ ),
1472
+ h(
1473
+ "div",
1474
+ { className: "dcpa-field" },
1475
+ h("label", { className: "dcpa-label" }, t("maxTokensLabel")),
1476
+ h("input", {
1477
+ type: "number",
1478
+ min: "1",
1479
+ className: "dcpa-input",
1480
+ value: draft.maxTokens ?? "",
1481
+ onChange: (e) => {
1482
+ const value = Number(e.target.value);
1483
+ if (Number.isInteger(value) && value > 0) {
1484
+ updateOverride(m, { maxTokens: value });
1485
+ }
1486
+ },
1487
+ }),
1488
+ h(
1489
+ "p",
1490
+ { className: "dcpa-hint" },
1491
+ `${t("discoveredValue")}: ${formatTokens(m.discovered?.maxTokens)}`
1492
+ )
1493
+ )
1494
+ ),
1495
+ h(
1496
+ "div",
1497
+ { className: "dcpa-switch-row" },
1498
+ h("div", { className: "dcpa-label" }, t("imageInputLabel")),
1499
+ h(
1500
+ "button",
1501
+ {
1502
+ type: "button",
1503
+ role: "switch",
1504
+ className: "dcpa-switch",
1505
+ "aria-checked": draft.image === true,
1506
+ "aria-label": t("imageInputLabel"),
1507
+ onClick: () => updateOverride(m, { image: draft.image !== true }),
1508
+ },
1509
+ h("span", { className: "dcpa-switch-thumb" })
1510
+ )
1511
+ ),
1512
+ h(
1513
+ "div",
1514
+ { className: "dcpa-field" },
1515
+ h("label", { className: "dcpa-label" }, t("reasoningLevelsLabel")),
1516
+ h(
1517
+ "div",
1518
+ { className: "dcpa-level-list" },
1519
+ REASONING_LEVELS.map((level) => {
1520
+ const active = draft.reasoningLevels.includes(level);
1521
+ return h(
1522
+ "label",
1523
+ { key: level, className: "dcpa-level" },
1524
+ h("input", {
1525
+ type: "checkbox",
1526
+ checked: active,
1527
+ onChange: () => {
1528
+ const current = draft.reasoningLevels;
1529
+ const next = active
1530
+ ? current.filter((item) => item !== level)
1531
+ : [...current, level];
1532
+ updateOverride(m, {
1533
+ reasoningLevels: REASONING_LEVELS.filter((item) => next.includes(item)),
1534
+ });
1535
+ },
1536
+ }),
1537
+ level
1538
+ );
1539
+ })
1540
+ )
1541
+ ),
1542
+ h(
1543
+ "div",
1544
+ { className: "dcpa-field" },
1545
+ h("label", { className: "dcpa-label" }, t("modelProtocolLabel")),
1546
+ h(
1547
+ "select",
1548
+ {
1549
+ className: "dcpa-select",
1550
+ value: modelOverrides[m.id]?.protocol ?? "",
1551
+ onChange: (e) => updateOverride(m, { protocol: e.target.value || undefined }),
1552
+ },
1553
+ h("option", { value: "" }, t("modelProtocolDefault")),
1554
+ PROTOCOLS.map((p) => h("option", { key: p, value: p }, protocolName(p)))
1555
+ ),
1556
+ h(
1557
+ "p",
1558
+ { className: "dcpa-hint" },
1559
+ `${t("modelProtocolHint")}${protocolName(m.nativeProtocol ?? "openai-completions")}`
1560
+ )
1561
+ ),
1562
+ h("p", { className: "dcpa-hint" }, t("paramsHint")),
1563
+ hasOverride
1564
+ ? h(
1565
+ "div",
1566
+ null,
1567
+ h(
1568
+ "button",
1569
+ {
1570
+ type: "button",
1571
+ className: "dcpa-btn dcpa-btn-sm",
1572
+ onClick: () => resetOverride(m.id),
1573
+ },
1574
+ t("resetParamsBtn")
1575
+ )
1576
+ )
1577
+ : null
1578
+ )
1579
+ : null
1580
+ );
1581
+ })
1582
+ )
1583
+ ),
1584
+
1585
+ // 3. Connection & Auth Card
1586
+ h(
1587
+ "div",
1588
+ { className: "dcpa-card" },
1589
+ h("h4", { className: "dcpa-section-title" }, t("sectionConnection")),
1590
+ h(
1591
+ "div",
1592
+ { className: "dcpa-switch-row" },
1593
+ h(
1594
+ "div",
1595
+ null,
1596
+ h("div", { className: "dcpa-label" }, t("enabledLabel")),
1597
+ h("p", { className: "dcpa-hint" }, t("enabledHint"))
1598
+ ),
1599
+ h(
1600
+ "button",
1601
+ {
1602
+ type: "button",
1603
+ role: "switch",
1604
+ className: "dcpa-switch",
1605
+ "aria-checked": enabled,
1606
+ "aria-label": t("enabledLabel"),
1607
+ onClick: () => setEnabled(!enabled),
1608
+ },
1609
+ h("span", { className: "dcpa-switch-thumb" })
1610
+ )
1611
+ ),
1612
+ h(
1613
+ "div",
1614
+ { className: "dcpa-field" },
1615
+ h("label", { className: "dcpa-label" }, t("baseUrlLabel")),
1616
+ h("input", {
1617
+ type: "text",
1618
+ className: "dcpa-input",
1619
+ value: baseURL,
1620
+ placeholder: effectiveUrl || t("baseUrlPlaceholder"),
1621
+ onChange: (e) => setBaseURL(e.target.value),
1622
+ }),
1623
+ h("p", { className: "dcpa-hint" }, t("baseUrlHint"))
1624
+ ),
1625
+ h(
1626
+ "div",
1627
+ { className: "dcpa-field" },
1628
+ h(
1629
+ "div",
1630
+ { className: "dcpa-label-row" },
1631
+ h("label", { className: "dcpa-label" }, `${t("apiKeyLabel")} (${cred.ref || apiKeyEnv})`),
1632
+ h(
1633
+ "span",
1634
+ { className: cred.configured ? "dcpa-badge dcpa-badge-ok" : "dcpa-badge" },
1635
+ cred.configured ? t("apiKeyConfiguredBadge") : t("apiKeyUnsetBadge")
1636
+ )
1637
+ ),
1638
+ h("input", {
1639
+ type: "password",
1640
+ className: "dcpa-input",
1641
+ autoComplete: "new-password",
1642
+ value: apiKeyDraft,
1643
+ placeholder: cred.configured ? t("apiKeySetPlaceholder") : t("apiKeyUnsetPlaceholder"),
1644
+ onChange: (e) => setApiKeyDraft(e.target.value),
1645
+ }),
1646
+ h("p", { className: "dcpa-hint" }, t("apiKeyHint"))
1647
+ ),
1648
+ h(
1649
+ "div",
1650
+ { className: "dcpa-grid" },
1651
+ h(
1652
+ "div",
1653
+ { className: "dcpa-field" },
1654
+ h("label", { className: "dcpa-label" }, t("protocolLabel")),
1655
+ h(
1656
+ "select",
1657
+ {
1658
+ className: "dcpa-select",
1659
+ value: protocol,
1660
+ onChange: (e) => setProtocol(e.target.value),
1661
+ },
1662
+ h("option", { value: "auto" }, t("protocolAuto")),
1663
+ PROTOCOLS.map((p) =>
1664
+ h("option", { key: p, value: p }, `${t("protocolFixedPrefix")}${protocolName(p)}`)
1665
+ )
1666
+ ),
1667
+ h("p", { className: "dcpa-hint" }, t("protocolHint"))
1668
+ ),
1669
+ h(
1670
+ "div",
1671
+ { className: "dcpa-field" },
1672
+ h("label", { className: "dcpa-label" }, t("displayNameLabel")),
1673
+ h("input", {
1674
+ type: "text",
1675
+ className: "dcpa-input",
1676
+ value: displayName,
1677
+ onChange: (e) => setDisplayName(e.target.value),
1678
+ }),
1679
+ h("p", { className: "dcpa-hint" }, t("displayNameHint"))
1680
+ )
1681
+ ),
1682
+ h(
1683
+ "details",
1684
+ null,
1685
+ h(
1686
+ "summary",
1687
+ { style: { cursor: "pointer", fontSize: "12px", fontWeight: 500 } },
1688
+ t("sectionAdvanced")
1689
+ ),
1690
+ h(
1691
+ "div",
1692
+ { style: { display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" } },
1693
+ h(
1694
+ "div",
1695
+ { className: "dcpa-grid" },
1696
+ h(
1697
+ "div",
1698
+ { className: "dcpa-field" },
1699
+ h("label", { className: "dcpa-label" }, t("includePrefixLabel")),
1700
+ h("input", {
1701
+ type: "text",
1702
+ className: "dcpa-input",
1703
+ value: includePrefix,
1704
+ placeholder: t("includePrefixPlaceholder"),
1705
+ onChange: (e) => setIncludePrefix(e.target.value),
1706
+ }),
1707
+ h("p", { className: "dcpa-hint" }, t("includePrefixHint"))
1708
+ ),
1709
+ h(
1710
+ "div",
1711
+ { className: "dcpa-field" },
1712
+ h("label", { className: "dcpa-label" }, t("excludePrefixLabel")),
1713
+ h("input", {
1714
+ type: "text",
1715
+ className: "dcpa-input",
1716
+ value: excludePrefix,
1717
+ placeholder: t("excludePrefixPlaceholder"),
1718
+ onChange: (e) => setExcludePrefix(e.target.value),
1719
+ }),
1720
+ h("p", { className: "dcpa-hint" }, t("excludePrefixHint"))
1721
+ )
1722
+ ),
1723
+ h(
1724
+ "div",
1725
+ { className: "dcpa-grid" },
1726
+ h(
1727
+ "div",
1728
+ { className: "dcpa-field" },
1729
+ h("label", { className: "dcpa-label" }, t("providerIdLabel")),
1730
+ h("input", {
1731
+ type: "text",
1732
+ className: "dcpa-input",
1733
+ value: provider,
1734
+ onChange: (e) => setProvider(e.target.value),
1735
+ }),
1736
+ h("p", { className: "dcpa-hint" }, t("providerIdHint"))
1737
+ ),
1738
+ h(
1739
+ "div",
1740
+ { className: "dcpa-field" },
1741
+ h("label", { className: "dcpa-label" }, t("apiKeyEnvLabel")),
1742
+ h("input", {
1743
+ type: "text",
1744
+ className: "dcpa-input",
1745
+ value: apiKeyEnv,
1746
+ onChange: (e) => setApiKeyEnv(e.target.value),
1747
+ }),
1748
+ h("p", { className: "dcpa-hint" }, t("apiKeyEnvHint"))
1749
+ )
1750
+ ),
1751
+ h(
1752
+ "div",
1753
+ { className: "dcpa-grid" },
1754
+ h(
1755
+ "div",
1756
+ { className: "dcpa-field" },
1757
+ h("label", { className: "dcpa-label" }, t("refreshIntervalLabel")),
1758
+ h("input", {
1759
+ type: "number",
1760
+ className: "dcpa-input",
1761
+ value: refreshIntervalSeconds,
1762
+ onChange: (e) => setRefreshIntervalSeconds(e.target.value),
1763
+ })
1764
+ )
1765
+ )
1766
+ )
1767
+ ),
1768
+ h(
1769
+ "div",
1770
+ { className: "dcpa-actions" },
1771
+ h(
1772
+ "button",
1773
+ {
1774
+ type: "button",
1775
+ className: "dcpa-btn dcpa-btn-primary",
1776
+ disabled: saving || syncing,
1777
+ onClick: () => onSave(false),
1778
+ },
1779
+ saving ? t("savingBtn") : t("saveBtn")
1780
+ ),
1781
+ cred.configured
1782
+ ? h(
1783
+ "button",
1784
+ {
1785
+ type: "button",
1786
+ className: "dcpa-btn",
1787
+ disabled: saving || syncing,
1788
+ onClick: () => onSave(true),
1789
+ },
1790
+ t("clearKeyBtn")
1791
+ )
1792
+ : null,
1793
+ h("span", { style: { flex: 1 } }),
1794
+ h(
1795
+ "button",
1796
+ {
1797
+ type: "button",
1798
+ className: "dcpa-btn dcpa-btn-danger",
1799
+ disabled: saving || syncing || loading,
1800
+ onClick: onRemove,
1801
+ },
1802
+ t("removeAccountBtn")
1803
+ )
1804
+ )
1805
+ )
1806
+ );
1807
+ }
1808
+
1809
+ const inject = ["slots", "locale"];
1810
+
1811
+ function apply(ctx) {
1812
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-cliproxyapi: dictionaries");
1813
+ const t = ctx.locale.bind(NS);
1814
+
1815
+ if (typeof document !== "undefined") {
1816
+ const styleId = "dsh-cliproxyapi-styles";
1817
+ let styleEl = document.getElementById(styleId);
1818
+ if (!styleEl) {
1819
+ styleEl = document.createElement("style");
1820
+ styleEl.id = styleId;
1821
+ styleEl.dataset.plugin = MODULE_ID;
1822
+ styleEl.textContent = CSS;
1823
+ document.head.appendChild(styleEl);
1824
+ } else {
1825
+ styleEl.textContent = CSS;
1826
+ }
1827
+ ctx.effect(
1828
+ () => () => {
1829
+ styleEl?.remove();
1830
+ },
1831
+ "dsh-cliproxyapi: styles"
1832
+ );
1833
+ }
1834
+
1835
+ // 1. Register on the Bundle detail page in Sidebar -> Plugins -> CLIProxyAPI
1836
+ ctx.slots.inject("plugins.bundle.config", () =>
1837
+ ctx.slots.register(
1838
+ {
1839
+ name: "plugins.bundle.config",
1840
+ key: MODULE_ID,
1841
+ locale: NS,
1842
+ },
1843
+ (ownerProps = {}) => (ownerProps.view === "summary" ? null : h(CpaSettingsPanel, { t }))
1844
+ )
1845
+ );
1846
+
1847
+ // 2. Register as a dedicated tab in Settings -> Built-in plugins -> CLIProxyAPI
1848
+ ctx.slots.inject("settings.plugins.tab", () =>
1849
+ ctx.slots.register(
1850
+ {
1851
+ name: "settings.plugins.tab",
1852
+ id: NS,
1853
+ order: 50,
1854
+ label: () => t("nav"),
1855
+ locale: NS,
1856
+ },
1857
+ () => h(CpaSettingsPanel, { t })
1858
+ )
1859
+ );
1860
+ }
1861
+
1862
+ exports.apply = apply;
1863
+ exports.inject = inject;
1864
+ exports.name = MODULE_ID;
1865
+ return module.exports;
1866
+ },
1867
+ });