@goodandready/dsh-clinebot 0.5.29 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/lib/client.js +387 -596
- package/lib/config.js +9 -0
- package/lib/index.js +31 -0
- package/lib/multimodal-budget.js +108 -0
- package/lib/proxy-token.js +2 -2
- package/lib/routes/proxy.js +11 -6
- package/lib/web-search.js +82 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -10,9 +10,6 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
|
|
11
11
|
// --- src/client/constants.js ---
|
|
12
12
|
const NS = 'dsh-clinebot'
|
|
13
|
-
// Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
|
|
14
|
-
const ROW_ID = 'dsh-clinebot'
|
|
15
|
-
const ROW_CONFIG_KEY = '@goodandready/dsh-clinebot#' + ROW_ID
|
|
16
13
|
const ROUTE_PREFIX = '/dsh-clinebot'
|
|
17
14
|
// Stable (frozen) fallback reference: useSyncExternalStore compares snapshots by
|
|
18
15
|
// identity (Object.is), so returning a fresh object literal causes an infinite render loop
|
|
@@ -24,6 +21,10 @@ function readConfigForms(ctx) {
|
|
|
24
21
|
try { return ctx.get('configForms') || undefined } catch { return undefined }
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
// Returns the namespace's ConfigFormController snapshot — `{ status, value, … }`.
|
|
25
|
+
// `status` is 'loading' | 'ready' | 'unavailable'; `value` is the decoded config
|
|
26
|
+
// object once the namespace is ready. Consumers that only need the status must
|
|
27
|
+
// read `snapshot?.status`, not the snapshot itself.
|
|
27
28
|
function useConfigFormsSnapshot(ctx) {
|
|
28
29
|
const scope = React.useMemo(() => {
|
|
29
30
|
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
@@ -34,11 +35,12 @@ function useConfigFormsSnapshot(ctx) {
|
|
|
34
35
|
const getSnapshot = React.useCallback(() => {
|
|
35
36
|
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
36
37
|
}, [scope])
|
|
37
|
-
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)
|
|
38
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY) || SNAPSHOT_READY
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// --- src/client/locales.js ---
|
|
41
42
|
const en = {
|
|
43
|
+
nav: 'ClineBot',
|
|
42
44
|
title: 'ClineBot',
|
|
43
45
|
subtitle: 'ClinePass subscription with dynamic plan models.',
|
|
44
46
|
'settings.loading': 'Loading ClineBot settings…',
|
|
@@ -53,7 +55,7 @@ const en = {
|
|
|
53
55
|
'badge.key_missing': 'Key missing',
|
|
54
56
|
'badge.registered': 'DSH Registered ({count} models)',
|
|
55
57
|
'badge.not_registered': 'Not registered',
|
|
56
|
-
'key.title': '
|
|
58
|
+
'key.title': 'Authorization & API Key',
|
|
57
59
|
'key.desc': 'The key is saved directly to DSH Credentials storage (~/.dsh/.credentials.yaml) and never exposed in plaintext configuration files.',
|
|
58
60
|
'key.placeholder_has': '••••••••••••••••••••••••',
|
|
59
61
|
'key.placeholder_empty': 'Paste ClinePass key (cline_...)',
|
|
@@ -61,7 +63,7 @@ const en = {
|
|
|
61
63
|
'key.hide': 'Hide',
|
|
62
64
|
'key.save': 'Save Key',
|
|
63
65
|
'key.saving': 'Saving…',
|
|
64
|
-
'key.login_fast': '
|
|
66
|
+
'key.login_fast': 'Open API Keys ↗',
|
|
65
67
|
'key.verifying': 'Validating key with Cline…',
|
|
66
68
|
'key.verified': '✓ Valid key for {email} ({plan})',
|
|
67
69
|
'key.invalid': '✗ Key validation failed: {error}',
|
|
@@ -69,7 +71,7 @@ const en = {
|
|
|
69
71
|
'key.get_key': 'Get key in app.cline.bot console ↗',
|
|
70
72
|
'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
|
|
71
73
|
'key.empty_err': 'Please enter an API key before saving',
|
|
72
|
-
'accounts.title': '
|
|
74
|
+
'accounts.title': 'Multi-Account Failover Pool',
|
|
73
75
|
'accounts.desc': 'Add extra accounts. When an account hits 429 rate limits, ClineBot switches to the next available account automatically.',
|
|
74
76
|
'accounts.active_badge': 'Active',
|
|
75
77
|
'accounts.pinned_badge': 'Pinned',
|
|
@@ -91,17 +93,17 @@ const en = {
|
|
|
91
93
|
'accounts.route_model_placeholder': 'Model ID (e.g. cline-pass/deepseek-v4-pro)',
|
|
92
94
|
'accounts.route_account_placeholder': 'Target Account',
|
|
93
95
|
'accounts.route_btn_add': 'Add Route',
|
|
94
|
-
'quota.title': '
|
|
96
|
+
'quota.title': 'ClinePass Quota & Rate Limits',
|
|
95
97
|
'quota.desc': 'Official rolling window request limits from ClinePass',
|
|
96
98
|
'quota.account': 'Account: {email} · Plan: {plan}',
|
|
97
99
|
'quota.refresh': 'Refresh Quotas',
|
|
98
100
|
'quota.refreshing': 'Refreshing…',
|
|
99
101
|
'quota.refreshed_msg': 'Subscription quotas refreshed',
|
|
100
102
|
'quota.window_5h': '⏱ 5-Hour Rolling Limit',
|
|
101
|
-
'quota.window_weekly': '
|
|
103
|
+
'quota.window_weekly': 'Weekly Window',
|
|
102
104
|
'quota.used': '{pct}% used',
|
|
103
105
|
'quota.reset_at': 'Reset: {time}',
|
|
104
|
-
'quota.window_monthly': '
|
|
106
|
+
'quota.window_monthly': 'Monthly Window',
|
|
105
107
|
'quota.countdown': 'Resets in {time}',
|
|
106
108
|
'quota.resetting': 'Resetting…',
|
|
107
109
|
'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
|
|
@@ -109,7 +111,7 @@ const en = {
|
|
|
109
111
|
'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
|
|
110
112
|
'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
111
113
|
'quota.warning_monthly': 'Low monthly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
112
|
-
'models.reasoning_tag': '
|
|
114
|
+
'models.reasoning_tag': 'Reasoning',
|
|
113
115
|
'models.effort_low': 'Low',
|
|
114
116
|
'models.effort_medium': 'Medium',
|
|
115
117
|
'models.effort_high': 'High',
|
|
@@ -133,10 +135,10 @@ const en = {
|
|
|
133
135
|
'models.show_all': 'Show All',
|
|
134
136
|
'models.clean_stale': 'Clean Stale ({count})',
|
|
135
137
|
'models.models_selected': '{selected} of {total} selected for chat',
|
|
136
|
-
'models.title': '
|
|
138
|
+
'models.title': 'Model Picker in DSH ({enabled} of {total} active)',
|
|
137
139
|
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
|
|
138
140
|
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
139
|
-
'models.sync': '
|
|
141
|
+
'models.sync': 'Check Plan Models',
|
|
140
142
|
'models.syncing': 'Checking…',
|
|
141
143
|
'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
|
|
142
144
|
'models.verified': 'Verified with plan',
|
|
@@ -162,7 +164,7 @@ const en = {
|
|
|
162
164
|
'models.btn_probe': 'Ping',
|
|
163
165
|
'models.star': 'Star',
|
|
164
166
|
'models.auto_new': 'New',
|
|
165
|
-
'models.apply_original_all': '
|
|
167
|
+
'models.apply_original_all': 'Original Specs',
|
|
166
168
|
'models.reset_contexts_all': '↺ Reset Contexts',
|
|
167
169
|
'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
|
|
168
170
|
'models.ctx_default': 'Default ({ctx} / {max})',
|
|
@@ -179,7 +181,7 @@ const en = {
|
|
|
179
181
|
'models.custom_max_label': 'Max Out',
|
|
180
182
|
'models.ctx_tag_original': 'Original',
|
|
181
183
|
'models.ctx_tag_custom': 'Custom',
|
|
182
|
-
'stats.title': '
|
|
184
|
+
'stats.title': 'Current DSH Session Telemetry',
|
|
183
185
|
'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
|
|
184
186
|
'stats.requests': 'Successful / Total Requests',
|
|
185
187
|
'stats.tokens': 'Estimated Session Tokens',
|
|
@@ -198,7 +200,7 @@ const en = {
|
|
|
198
200
|
'stats.reset_btn': 'Reset Stats',
|
|
199
201
|
'stats.confirm_reset': 'Confirm Reset?',
|
|
200
202
|
'stats.cancel_reset': 'Cancel',
|
|
201
|
-
'diag.title': '
|
|
203
|
+
'diag.title': 'Diagnostics & Auto-Registration',
|
|
202
204
|
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
|
|
203
205
|
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
204
206
|
'diag.smoke_testing': 'Testing…',
|
|
@@ -234,13 +236,6 @@ const en = {
|
|
|
234
236
|
'dashboard.account_pool': 'Active Routing Account',
|
|
235
237
|
'dashboard.credits': 'Credit Balance',
|
|
236
238
|
'dashboard.updated_at': 'Updated at',
|
|
237
|
-
'card.title': 'ClineBot Provider',
|
|
238
|
-
'card.status_connected': 'Connected',
|
|
239
|
-
'card.status_no_key': 'Key missing',
|
|
240
|
-
'card.open_settings': 'Settings & Accounts',
|
|
241
|
-
'card.save_key': 'Save Key',
|
|
242
|
-
'card.key_saved': 'Key saved',
|
|
243
|
-
'card.models_active': '{count} active models',
|
|
244
239
|
'composer.cost_title': 'Estimated Session Cost',
|
|
245
240
|
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
246
241
|
'composer.prompt_tok': 'Prompt Tokens',
|
|
@@ -254,6 +249,7 @@ const en = {
|
|
|
254
249
|
}
|
|
255
250
|
|
|
256
251
|
const zh = {
|
|
252
|
+
nav: 'ClineBot',
|
|
257
253
|
title: 'ClineBot',
|
|
258
254
|
subtitle: 'ClinePass 订阅服务,包含动态套餐模型。',
|
|
259
255
|
'settings.loading': '正在加载 ClineBot 设置…',
|
|
@@ -268,7 +264,7 @@ const en = {
|
|
|
268
264
|
'badge.key_missing': '未配置密钥',
|
|
269
265
|
'badge.registered': '已接入 DSH ({count} 个模型)',
|
|
270
266
|
'badge.not_registered': '未接入 DSH',
|
|
271
|
-
'key.title': '
|
|
267
|
+
'key.title': '授权与 API 密钥',
|
|
272
268
|
'key.desc': '密钥直接加密保存在 DSH 凭据管理器 (~/.dsh/.credentials.yaml),绝不会以明文形式暴露在普通配置文件中。',
|
|
273
269
|
'key.placeholder_has': '••••••••••••••••••••••••',
|
|
274
270
|
'key.placeholder_empty': '粘贴 ClinePass 密钥 (cline_...)',
|
|
@@ -276,7 +272,7 @@ const en = {
|
|
|
276
272
|
'key.hide': '隐藏',
|
|
277
273
|
'key.save': '保存密钥',
|
|
278
274
|
'key.saving': '正在保存…',
|
|
279
|
-
'key.login_fast': '
|
|
275
|
+
'key.login_fast': '打开 API 密钥页 ↗',
|
|
280
276
|
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
281
277
|
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
282
278
|
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
@@ -284,7 +280,7 @@ const en = {
|
|
|
284
280
|
'key.get_key': '前往 app.cline.bot 控制台获取密钥 ↗',
|
|
285
281
|
'key.saved_msg': '密钥已成功保存至 DSH 凭据服务。验证状态:{status}',
|
|
286
282
|
'key.empty_err': '保存前请输入 API 密钥',
|
|
287
|
-
'accounts.title': '
|
|
283
|
+
'accounts.title': '多账号故障转移与轮换池 (Failover)',
|
|
288
284
|
'accounts.desc': '添加备用账号。当某个账号触发 429 速率限制或配额耗尽时,ClineBot 会智能无缝切换至剩余额度最高的备用账号。',
|
|
289
285
|
'accounts.active_badge': '当前活动',
|
|
290
286
|
'accounts.pinned_badge': '固定使用',
|
|
@@ -306,17 +302,17 @@ const en = {
|
|
|
306
302
|
'accounts.route_model_placeholder': '模型 ID (例如 cline-pass/deepseek-v4-pro)',
|
|
307
303
|
'accounts.route_account_placeholder': '目标账号',
|
|
308
304
|
'accounts.route_btn_add': '添加路由',
|
|
309
|
-
'quota.title': '
|
|
305
|
+
'quota.title': 'ClinePass 用量配额与限额监控',
|
|
310
306
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
311
307
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
312
308
|
'quota.refresh': '刷新配额',
|
|
313
309
|
'quota.refreshing': '正在刷新…',
|
|
314
310
|
'quota.refreshed_msg': '订阅用量配额已刷新',
|
|
315
311
|
'quota.window_5h': '⏱ 5 小时滚动限额',
|
|
316
|
-
'quota.window_weekly': '
|
|
312
|
+
'quota.window_weekly': '每周滚动窗口',
|
|
317
313
|
'quota.used': '已使用 {pct}%',
|
|
318
314
|
'quota.reset_at': '重置时间:{time}',
|
|
319
|
-
'quota.window_monthly': '
|
|
315
|
+
'quota.window_monthly': '每月滚动窗口',
|
|
320
316
|
'quota.countdown': '{time}后重置',
|
|
321
317
|
'quota.resetting': '正在重置',
|
|
322
318
|
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
@@ -324,7 +320,7 @@ const en = {
|
|
|
324
320
|
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
325
321
|
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
326
322
|
'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
327
|
-
'models.reasoning_tag': '
|
|
323
|
+
'models.reasoning_tag': '深度思考',
|
|
328
324
|
'models.effort_low': '低',
|
|
329
325
|
'models.effort_medium': '中',
|
|
330
326
|
'models.effort_high': '高',
|
|
@@ -348,10 +344,10 @@ const en = {
|
|
|
348
344
|
'models.show_all': '显示全部',
|
|
349
345
|
'models.clean_stale': '清理过期 ({count})',
|
|
350
346
|
'models.models_selected': '已选择 {selected} / {total} 个模型用于聊天',
|
|
351
|
-
'models.title': '
|
|
347
|
+
'models.title': 'DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
352
348
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
353
349
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
354
|
-
'models.sync': '
|
|
350
|
+
'models.sync': '同步订阅模型',
|
|
355
351
|
'models.syncing': '正在同步…',
|
|
356
352
|
'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
|
|
357
353
|
'models.verified': '已与套餐同步',
|
|
@@ -377,7 +373,7 @@ const en = {
|
|
|
377
373
|
'models.btn_probe': '测试延迟',
|
|
378
374
|
'models.star': '推荐',
|
|
379
375
|
'models.auto_new': '新上线',
|
|
380
|
-
'models.apply_original_all': '
|
|
376
|
+
'models.apply_original_all': '原厂规格',
|
|
381
377
|
'models.reset_contexts_all': '↺ 重置上下文',
|
|
382
378
|
'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
|
|
383
379
|
'models.ctx_default': '默认 ({ctx} / {max})',
|
|
@@ -394,7 +390,7 @@ const en = {
|
|
|
394
390
|
'models.custom_max_label': '最大输出',
|
|
395
391
|
'models.ctx_tag_original': '原厂',
|
|
396
392
|
'models.ctx_tag_custom': '自定义',
|
|
397
|
-
'stats.title': '
|
|
393
|
+
'stats.title': '当前 DSH 会话请求遥测',
|
|
398
394
|
'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
|
|
399
395
|
'stats.requests': '成功 / 总请求次数',
|
|
400
396
|
'stats.tokens': '会话累计 Token 估算',
|
|
@@ -413,7 +409,7 @@ const en = {
|
|
|
413
409
|
'stats.reset_btn': '重置统计',
|
|
414
410
|
'stats.confirm_reset': '确认重置?',
|
|
415
411
|
'stats.cancel_reset': '取消',
|
|
416
|
-
'diag.title': '
|
|
412
|
+
'diag.title': '诊断工具与服务接入',
|
|
417
413
|
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试或强制重新同步。',
|
|
418
414
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
419
415
|
'diag.smoke_testing': '正在测试…',
|
|
@@ -449,13 +445,6 @@ const en = {
|
|
|
449
445
|
'dashboard.account_pool': '当前承载账户',
|
|
450
446
|
'dashboard.credits': '点数余额',
|
|
451
447
|
'dashboard.updated_at': '更新于',
|
|
452
|
-
'card.title': 'ClineBot 模型服务',
|
|
453
|
-
'card.status_connected': '已连接',
|
|
454
|
-
'card.status_no_key': '未配置密钥',
|
|
455
|
-
'card.open_settings': '完整设置与多账户',
|
|
456
|
-
'card.save_key': '保存密钥',
|
|
457
|
-
'card.key_saved': '密钥已保存',
|
|
458
|
-
'card.models_active': '{count} 个可用模型',
|
|
459
448
|
'composer.cost_title': '预估会话成本',
|
|
460
449
|
'composer.cost_breakdown': 'Token 与费用明细',
|
|
461
450
|
'composer.prompt_tok': '提示词 Token',
|
|
@@ -483,28 +472,50 @@ const en = {
|
|
|
483
472
|
}
|
|
484
473
|
}
|
|
485
474
|
|
|
486
|
-
// --- src/client/
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
stroke: 'currentColor',
|
|
494
|
-
strokeWidth: 1.5,
|
|
495
|
-
strokeLinecap: 'round',
|
|
496
|
-
strokeLinejoin: 'round',
|
|
497
|
-
})
|
|
498
|
-
)
|
|
499
|
-
}
|
|
500
|
-
let ChevronIcon = null
|
|
475
|
+
// --- src/client/primitives.js ---
|
|
476
|
+
// The shell's module table provides the design-system primitives, so the plugin
|
|
477
|
+
// renders the same controls (and inherits the same hover/focus/disabled treatment)
|
|
478
|
+
// as the native settings surfaces instead of re-drawing them. Every lookup is
|
|
479
|
+
// defensive: a missing primitive degrades to the plugin's own styled fallback
|
|
480
|
+
// rather than failing the render.
|
|
481
|
+
let primitives = null
|
|
501
482
|
try {
|
|
502
|
-
|
|
503
|
-
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
|
|
483
|
+
primitives = require('@deepseek-ai/dsh-client-ui-primitives') || null
|
|
504
484
|
} catch (_) {
|
|
505
|
-
|
|
485
|
+
primitives = null
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Native toggle for boolean preferences; falls back to a checkbox. */
|
|
489
|
+
const Switch = (primitives && primitives.Switch) || null
|
|
490
|
+
|
|
491
|
+
/** Native dialog window; the plugin draws a plainer one when it is absent. */
|
|
492
|
+
const Modal = (primitives && primitives.Modal) || null
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Boolean preference control: the native Switch when the shell provides it, the
|
|
496
|
+
* plugin's own checkbox otherwise. Both take the same `checked`/`disabled`/`onChange`.
|
|
497
|
+
* @param props - checked state, change handler, accessible label and disabled flag.
|
|
498
|
+
* @returns the control element.
|
|
499
|
+
*/
|
|
500
|
+
function BooleanControl(props) {
|
|
501
|
+
const { checked, disabled, label, onChange } = props
|
|
502
|
+
if (Switch) {
|
|
503
|
+
return React.createElement(Switch, {
|
|
504
|
+
checked: !!checked,
|
|
505
|
+
disabled: !!disabled,
|
|
506
|
+
label,
|
|
507
|
+
onChange: (next) => { if (typeof onChange === 'function') onChange(next) },
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
return React.createElement('input', {
|
|
511
|
+
type: 'checkbox',
|
|
512
|
+
style: { cursor: disabled ? 'not-allowed' : 'pointer', width: '16px', height: '16px', flex: 'none' },
|
|
513
|
+
checked: !!checked,
|
|
514
|
+
disabled: !!disabled,
|
|
515
|
+
'aria-label': label,
|
|
516
|
+
onChange: (e) => { if (typeof onChange === 'function') onChange(e.target.checked) },
|
|
517
|
+
})
|
|
506
518
|
}
|
|
507
|
-
const Chevron = ChevronIcon || FallbackChevron
|
|
508
519
|
|
|
509
520
|
// --- src/client/styles.js ---
|
|
510
521
|
function ensureCss() {
|
|
@@ -514,88 +525,97 @@ function ensureCss() {
|
|
|
514
525
|
style.id = 'dsh-clinebot-full-css'
|
|
515
526
|
style.dataset.dshPlugin = NS
|
|
516
527
|
style.textContent = `
|
|
517
|
-
.cb-page{display:flex;flex-direction:column;gap:
|
|
518
|
-
.cb-
|
|
519
|
-
.cb-
|
|
520
|
-
.cb-
|
|
521
|
-
|
|
522
|
-
.cb-
|
|
523
|
-
|
|
524
|
-
.cb-section-
|
|
525
|
-
|
|
526
|
-
.cb-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
.cb-
|
|
530
|
-
|
|
531
|
-
.cb-badge{
|
|
532
|
-
.cb-badge-
|
|
533
|
-
|
|
534
|
-
.cb-
|
|
535
|
-
|
|
536
|
-
.cb-input{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;width:100%;box-sizing:border-box}
|
|
528
|
+
.cb-page{display:flex;flex-direction:column;gap:16px;width:100%}
|
|
529
|
+
.cb-header{display:flex;flex-direction:column;gap:4px;padding:2px 0 4px}
|
|
530
|
+
.cb-page-title{display:flex;flex-wrap:wrap;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:24px}
|
|
531
|
+
.cb-page-sub{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
532
|
+
|
|
533
|
+
.cb-section-card{display:flex;flex-direction:column;gap:12px;padding:12px 14px;border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-settings-card-fill)}
|
|
534
|
+
.cb-section-title{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 12px;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
|
|
535
|
+
.cb-section-desc{margin-top:-6px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
536
|
+
|
|
537
|
+
.cb-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
538
|
+
.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
|
|
539
|
+
|
|
540
|
+
.cb-badge{display:inline-flex;align-items:center;gap:4px;flex:none;padding:1px 6px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xs);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;font-weight:400}
|
|
541
|
+
.cb-badge-ok{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 45%, transparent)}
|
|
542
|
+
.cb-badge-warn{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent)}
|
|
543
|
+
.cb-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent)}
|
|
544
|
+
|
|
545
|
+
.cb-input{box-sizing:border-box;width:100%;height:36px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px}
|
|
546
|
+
.cb-input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
537
547
|
.cb-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}
|
|
538
|
-
.cb-input-group{display:flex;
|
|
539
|
-
|
|
540
|
-
.cb-
|
|
541
|
-
|
|
542
|
-
.cb-btn-
|
|
543
|
-
.cb-btn
|
|
544
|
-
.cb-btn-
|
|
545
|
-
.cb-btn
|
|
546
|
-
.cb-btn-
|
|
547
|
-
|
|
548
|
-
.cb-
|
|
549
|
-
.cb-
|
|
548
|
+
.cb-input-group{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
549
|
+
.cb-input-group > .cb-input{flex:1 1 240px;min-width:0}
|
|
550
|
+
.cb-input-group > .cb-btn{flex:none}
|
|
551
|
+
|
|
552
|
+
.cb-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:36px;padding:0 14px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;white-space:nowrap;cursor:pointer}
|
|
553
|
+
.cb-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
554
|
+
.cb-btn:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
555
|
+
.cb-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
556
|
+
.cb-btn-primary{border-color:transparent;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-2)}
|
|
557
|
+
.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary);opacity:.88}
|
|
558
|
+
.cb-btn-danger{color:var(--dsw-alias-state-error-primary)}
|
|
559
|
+
.cb-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)}
|
|
560
|
+
.cb-btn-disabled{opacity:.5;cursor:not-allowed}
|
|
561
|
+
|
|
562
|
+
.cb-progress-card, .cb-bar-container{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}
|
|
563
|
+
.cb-progress-head, .cb-bar-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;width:100%;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:20px}
|
|
550
564
|
.cb-progress-label{font-weight:500;white-space:nowrap}
|
|
551
|
-
.cb-progress-meta, .cb-bar-meta{
|
|
552
|
-
.cb-progress-track, .cb-bar-track{width:100%;height:
|
|
553
|
-
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:
|
|
554
|
-
.cb-progress-bar-warn{height:100%;border-radius:
|
|
555
|
-
.cb-progress-bar-bad{height:100%;border-radius:
|
|
556
|
-
|
|
557
|
-
.cb-table{width:100%;border-collapse:collapse;margin-top:
|
|
558
|
-
.cb-table th{
|
|
559
|
-
.cb-table td{padding:10px;border-bottom
|
|
560
|
-
.cb-table tr:hover{background:var(--dsw-alias-bg-
|
|
561
|
-
|
|
562
|
-
.cb-alert
|
|
563
|
-
.cb-alert-
|
|
564
|
-
.cb-alert-
|
|
565
|
-
.cb-
|
|
566
|
-
.cb-banner-
|
|
567
|
-
.cb-
|
|
568
|
-
.cb-stat-
|
|
569
|
-
.cb-stat-
|
|
570
|
-
.cb-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
.
|
|
576
|
-
|
|
577
|
-
.
|
|
578
|
-
.cb-
|
|
579
|
-
.cb-
|
|
565
|
+
.cb-progress-meta, .cb-bar-meta{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap}
|
|
566
|
+
.cb-progress-track, .cb-bar-track{width:100%;height:6px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
567
|
+
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
568
|
+
.cb-progress-bar-warn{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-state-warn-primary);transition:width .3s ease}
|
|
569
|
+
.cb-progress-bar-bad{height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-state-error-primary);transition:width .3s ease}
|
|
570
|
+
|
|
571
|
+
.cb-table{width:100%;border-collapse:collapse;margin-top:4px}
|
|
572
|
+
.cb-table th{padding:8px 10px;border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);text-align:left;font-size:12px;font-weight:500;line-height:18px}
|
|
573
|
+
.cb-table td{padding:10px;border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-size:14px;line-height:20px}
|
|
574
|
+
.cb-table tr:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
575
|
+
|
|
576
|
+
.cb-alert{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:var(--dsw-radius-md);font-size:12px;line-height:18px}
|
|
577
|
+
.cb-alert-ok{padding:10px 12px;border-radius:var(--dsw-radius-md);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:12px;line-height:18px}
|
|
578
|
+
.cb-alert-bad{padding:10px 12px;border-radius:var(--dsw-radius-md);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
|
|
579
|
+
.cb-alert-err{padding:10px 12px;border-radius:var(--dsw-radius-md);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
|
|
580
|
+
.cb-banner-warning{display:flex;align-items:center;gap:8px;padding:10px 12px;border:.5px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent);border-radius:var(--dsw-radius-md);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent);color:var(--dsw-alias-state-warn-primary);font-size:12px;font-weight:500;line-height:18px}
|
|
581
|
+
.cb-banner-exhausted{display:flex;align-items:center;gap:8px;padding:10px 12px;border:.5px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);border-radius:var(--dsw-radius-md);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;font-weight:600;line-height:18px}
|
|
582
|
+
.cb-stat-box{display:flex;flex-direction:column;gap:2px;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1)}
|
|
583
|
+
.cb-stat-val{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:24px}
|
|
584
|
+
.cb-stat-lbl{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
585
|
+
.cb-preview{padding:10px 12px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);font-family:var(--ds-font-family-code,monospace);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-all}
|
|
586
|
+
|
|
587
|
+
/* The shell lays the footer action list out as a row, which a full-width meter
|
|
588
|
+
cannot share: with another plugin's entry beside it the plan name truncates and
|
|
589
|
+
the window label wraps. Stack the list while this meter is in it. The :has()
|
|
590
|
+
scoping keeps the rule off any footer that does not contain this plugin, so it
|
|
591
|
+
cannot reach across other registrants the way a plain descendant selector would. */
|
|
592
|
+
[class*="_footerActions"]:has(.cb-foot), [class*="_footerActions"]:has(.cb-railButton){flex-direction:column}
|
|
593
|
+
.cb-foot{box-sizing:border-box;flex:1 1 auto;width:100%;min-width:0;display:flex;flex-direction:column;gap:6px;margin:0 0 4px;padding:8px;border:.5px solid transparent;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}
|
|
594
|
+
.cb-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
|
|
595
|
+
.cb-foot:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
596
|
+
.cb-footTop{display:flex;align-items:center;gap:8px;min-width:0}
|
|
597
|
+
.cb-footName{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:20px;white-space:nowrap;text-overflow:ellipsis}
|
|
598
|
+
.cb-footRow{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
599
|
+
.cb-footHead{display:flex;align-items:baseline;gap:8px;min-width:0}
|
|
580
600
|
.cb-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
581
601
|
.cb-footAmount{flex:none;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
582
|
-
.cb-footBar{display:block;background:var(--dsw-alias-bg-layer-2);
|
|
583
|
-
.cb-footFill{display:block;background:var(--dsw-alias-brand-primary);
|
|
602
|
+
.cb-footBar{display:block;height:4px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
603
|
+
.cb-footFill{display:block;height:100%;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-brand-primary);transition:width .3s ease}
|
|
584
604
|
.cb-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
585
605
|
.cb-footPct{flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:12px;line-height:16px;font-variant-numeric:tabular-nums}
|
|
586
|
-
.cb-railButton{box-sizing:border-box;
|
|
606
|
+
.cb-railButton{box-sizing:border-box;flex:none;display:inline-flex;justify-content:center;align-items:center;width:36px;height:36px;margin:0 0 4px;padding:0;border:.5px solid transparent;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
587
607
|
.cb-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
588
|
-
.cb-railButton:focus-visible{outline:
|
|
589
|
-
.cb-glyph{flex:none;justify-content:center;align-items:center;
|
|
608
|
+
.cb-railButton:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
609
|
+
.cb-glyph{flex:none;display:inline-flex;justify-content:center;align-items:center;color:var(--dsw-alias-brand-primary)}
|
|
590
610
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
.cb-
|
|
594
|
-
.cb-
|
|
595
|
-
.cb-
|
|
596
|
-
.cb-
|
|
611
|
+
/* The window chrome and its width belong to the shell's Modal, so the body only
|
|
612
|
+
fills whatever it is given — a fixed width here overflows the panel. */
|
|
613
|
+
.cb-dialog{display:flex;flex-direction:column;gap:12px;width:100%;min-width:0;max-height:min(72vh, 640px);overflow-y:auto;color:var(--dsw-alias-label-primary)}
|
|
614
|
+
.cb-dialogMeta{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}
|
|
615
|
+
.cb-dialogCard{display:flex;flex-direction:column;gap:12px;padding:12px 14px;border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-settings-card-fill)}
|
|
616
|
+
.cb-dialogFallback{position:fixed;inset:0;z-index:900;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1)}
|
|
617
|
+
.cb-dialogFallbackPanel{display:flex;flex-direction:column;gap:12px;max-height:calc(100vh - 96px);overflow-y:auto;padding:18px;border-radius:var(--dsw-radius-panel);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent)}
|
|
597
618
|
|
|
598
|
-
.cb-providerCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px;margin:8px 0}
|
|
599
619
|
`
|
|
600
620
|
document.head.appendChild(style)
|
|
601
621
|
}
|
|
@@ -1533,18 +1553,17 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1533
1553
|
),
|
|
1534
1554
|
React.createElement(
|
|
1535
1555
|
'div',
|
|
1536
|
-
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '4px', paddingTop: '
|
|
1556
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px', marginTop: '4px', paddingTop: '16px', borderTop: '.5px solid var(--dsw-alias-border-l2)' } },
|
|
1537
1557
|
React.createElement(
|
|
1538
1558
|
'div',
|
|
1539
|
-
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1540
|
-
React.createElement('span', { style: { fontSize: '
|
|
1541
|
-
React.createElement('span', { style: { fontSize: '
|
|
1559
|
+
{ style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
|
|
1560
|
+
React.createElement('span', { style: { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary)' } }, t('sidebar.show_quota')),
|
|
1561
|
+
React.createElement('span', { style: { marginTop: '4px', fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' } }, t('sidebar.show_quota_hint'))
|
|
1542
1562
|
),
|
|
1543
|
-
React.createElement(
|
|
1544
|
-
type: 'checkbox',
|
|
1545
|
-
style: { cursor: 'pointer', width: '16px', height: '16px' },
|
|
1563
|
+
React.createElement(BooleanControl, {
|
|
1546
1564
|
checked: showSidebarQuota !== false,
|
|
1547
|
-
|
|
1565
|
+
label: t('sidebar.show_quota'),
|
|
1566
|
+
onChange: (next) => onToggleSidebarQuota && onToggleSidebarQuota(next),
|
|
1548
1567
|
})
|
|
1549
1568
|
)
|
|
1550
1569
|
)
|
|
@@ -1814,17 +1833,21 @@ function VisibleModelsSub({
|
|
|
1814
1833
|
},
|
|
1815
1834
|
React.createElement(
|
|
1816
1835
|
'div',
|
|
1817
|
-
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '
|
|
1836
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' } },
|
|
1818
1837
|
React.createElement(
|
|
1819
|
-
'
|
|
1820
|
-
{ style: { display: 'flex', alignItems: 'center', gap: '
|
|
1821
|
-
React.createElement(
|
|
1822
|
-
|
|
1838
|
+
'div',
|
|
1839
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 } },
|
|
1840
|
+
React.createElement(
|
|
1841
|
+
'span',
|
|
1842
|
+
{ style: { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary)' } },
|
|
1843
|
+
t('models.filter_by_plan')
|
|
1844
|
+
),
|
|
1845
|
+
React.createElement(BooleanControl, {
|
|
1823
1846
|
checked: !!filterModelsByPlan,
|
|
1824
1847
|
disabled: !!busy,
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1848
|
+
label: t('models.filter_by_plan'),
|
|
1849
|
+
onChange: (next) => onConfigUpdate && onConfigUpdate({ filterModelsByPlan: next }),
|
|
1850
|
+
})
|
|
1828
1851
|
),
|
|
1829
1852
|
React.createElement(
|
|
1830
1853
|
'div',
|
|
@@ -2668,25 +2691,25 @@ function DiagSection({
|
|
|
2668
2691
|
)
|
|
2669
2692
|
),
|
|
2670
2693
|
React.createElement(
|
|
2671
|
-
'
|
|
2672
|
-
{ style: { display: '
|
|
2673
|
-
React.createElement('input', {
|
|
2674
|
-
type: 'checkbox',
|
|
2675
|
-
checked: !!proxyMode,
|
|
2676
|
-
disabled: !!busy,
|
|
2677
|
-
onChange: (e) => onToggleProxy && onToggleProxy(e.target.checked),
|
|
2678
|
-
}),
|
|
2694
|
+
'div',
|
|
2695
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px' } },
|
|
2679
2696
|
React.createElement(
|
|
2680
2697
|
'span',
|
|
2681
2698
|
{
|
|
2682
2699
|
style: {
|
|
2683
|
-
fontSize: '
|
|
2684
|
-
|
|
2700
|
+
fontSize: '14px',
|
|
2701
|
+
lineHeight: '20px',
|
|
2685
2702
|
color: proxyMode ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
2686
2703
|
},
|
|
2687
2704
|
},
|
|
2688
2705
|
proxyMode ? t('diag.proxy_enabled') : t('diag.proxy_disabled')
|
|
2689
|
-
)
|
|
2706
|
+
),
|
|
2707
|
+
React.createElement(BooleanControl, {
|
|
2708
|
+
checked: !!proxyMode,
|
|
2709
|
+
disabled: !!busy,
|
|
2710
|
+
label: t('diag.proxy_title'),
|
|
2711
|
+
onChange: (next) => onToggleProxy && onToggleProxy(next),
|
|
2712
|
+
})
|
|
2690
2713
|
)
|
|
2691
2714
|
),
|
|
2692
2715
|
React.createElement(
|
|
@@ -2779,10 +2802,14 @@ function MiniRing({ percent, size = 18, warn = false }) {
|
|
|
2779
2802
|
)
|
|
2780
2803
|
}
|
|
2781
2804
|
|
|
2782
|
-
function SidebarQuota({ ctx, wide,
|
|
2805
|
+
function SidebarQuota({ ctx, wide, t: propT }) {
|
|
2783
2806
|
const t = propT || makeT(en, en)
|
|
2784
2807
|
const [data, setData] = React.useState(null)
|
|
2785
|
-
const [
|
|
2808
|
+
const [open, setOpen] = React.useState(false)
|
|
2809
|
+
|
|
2810
|
+
// Check the configForms snapshot for the showSidebarQuota toggle
|
|
2811
|
+
const configSnapshot = useConfigFormsSnapshot(ctx)
|
|
2812
|
+
const show = configSnapshot?.value?.showSidebarQuota !== false
|
|
2786
2813
|
|
|
2787
2814
|
const loadQuota = React.useCallback(async () => {
|
|
2788
2815
|
try {
|
|
@@ -2795,7 +2822,9 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2795
2822
|
}
|
|
2796
2823
|
}, [])
|
|
2797
2824
|
|
|
2825
|
+
// Poll only while the meter is on screen — a hidden meter has nothing to refresh.
|
|
2798
2826
|
React.useEffect(() => {
|
|
2827
|
+
if (!show) return undefined
|
|
2799
2828
|
let timer = null
|
|
2800
2829
|
const scheduleNext = () => {
|
|
2801
2830
|
timer = setTimeout(async () => {
|
|
@@ -2806,12 +2835,9 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2806
2835
|
loadQuota()
|
|
2807
2836
|
scheduleNext()
|
|
2808
2837
|
return () => { if (timer) clearTimeout(timer) }
|
|
2809
|
-
}, [loadQuota])
|
|
2838
|
+
}, [loadQuota, show])
|
|
2810
2839
|
|
|
2811
|
-
|
|
2812
|
-
const config = useConfigFormsSnapshot(ctx)
|
|
2813
|
-
const show = config?.showSidebarQuota !== false
|
|
2814
|
-
if (!show || !visible) return null
|
|
2840
|
+
if (!show) return null
|
|
2815
2841
|
|
|
2816
2842
|
const fiveHour = data?.windows?.fiveHour
|
|
2817
2843
|
const weekly = data?.windows?.weekly
|
|
@@ -2820,85 +2846,101 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2820
2846
|
const isWarn = pct >= 85
|
|
2821
2847
|
const planName = data?.plan || t('title')
|
|
2822
2848
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
} else {
|
|
2827
|
-
ctx?.get?.('layout')?.selectPanel?.('clinebot-dashboard')
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2849
|
+
// The window is local to the meter: nothing is selected or navigated, so the
|
|
2850
|
+
// conversation stays where it is and closing simply drops the window.
|
|
2851
|
+
const handleOpen = () => setOpen(true)
|
|
2830
2852
|
|
|
2831
2853
|
const tooltip = `${t('sidebar.quota_title')}: ${pct}% ${t('dashboard.used')} (${planName})`
|
|
2854
|
+
const dialog = React.createElement(DashboardDialog, { t, open, onClose: () => setOpen(false) })
|
|
2832
2855
|
|
|
2833
2856
|
if (!wide) {
|
|
2834
2857
|
return React.createElement(
|
|
2858
|
+
React.Fragment,
|
|
2859
|
+
null,
|
|
2860
|
+
React.createElement(
|
|
2861
|
+
'button',
|
|
2862
|
+
{
|
|
2863
|
+
type: 'button',
|
|
2864
|
+
className: 'cb-railButton',
|
|
2865
|
+
'aria-label': tooltip,
|
|
2866
|
+
'aria-expanded': open,
|
|
2867
|
+
title: tooltip,
|
|
2868
|
+
onClick: handleOpen,
|
|
2869
|
+
},
|
|
2870
|
+
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 20 })
|
|
2871
|
+
),
|
|
2872
|
+
dialog
|
|
2873
|
+
)
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
return React.createElement(
|
|
2877
|
+
React.Fragment,
|
|
2878
|
+
null,
|
|
2879
|
+
React.createElement(
|
|
2835
2880
|
'button',
|
|
2836
2881
|
{
|
|
2837
2882
|
type: 'button',
|
|
2838
|
-
className: 'cb-
|
|
2883
|
+
className: 'cb-foot',
|
|
2839
2884
|
'aria-label': tooltip,
|
|
2885
|
+
'aria-expanded': open,
|
|
2840
2886
|
title: tooltip,
|
|
2841
2887
|
onClick: handleOpen,
|
|
2842
2888
|
},
|
|
2843
|
-
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 20 })
|
|
2844
|
-
)
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
return React.createElement(
|
|
2848
|
-
'button',
|
|
2849
|
-
{
|
|
2850
|
-
type: 'button',
|
|
2851
|
-
className: 'cb-foot',
|
|
2852
|
-
'aria-label': tooltip,
|
|
2853
|
-
title: tooltip,
|
|
2854
|
-
onClick: handleOpen,
|
|
2855
|
-
},
|
|
2856
|
-
React.createElement(
|
|
2857
|
-
'div',
|
|
2858
|
-
{ className: 'cb-footTop' },
|
|
2859
|
-
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 16 }),
|
|
2860
|
-
React.createElement('span', { className: 'cb-footName' }, planName),
|
|
2861
|
-
React.createElement('span', { className: 'cb-footPct', style: { marginLeft: 'auto', fontWeight: 600, color: isWarn ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-label-secondary)' } }, `${pct}%`)
|
|
2862
|
-
),
|
|
2863
|
-
React.createElement(
|
|
2864
|
-
'div',
|
|
2865
|
-
{ className: 'cb-footRow' },
|
|
2866
2889
|
React.createElement(
|
|
2867
2890
|
'div',
|
|
2868
|
-
{ className: 'cb-
|
|
2869
|
-
React.createElement(
|
|
2870
|
-
React.createElement('span', { className: 'cb-
|
|
2891
|
+
{ className: 'cb-footTop' },
|
|
2892
|
+
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 16 }),
|
|
2893
|
+
React.createElement('span', { className: 'cb-footName' }, planName),
|
|
2894
|
+
React.createElement('span', { className: 'cb-footPct', style: { marginLeft: 'auto', fontWeight: 600, color: isWarn ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-label-secondary)' } }, `${pct}%`)
|
|
2871
2895
|
),
|
|
2872
2896
|
React.createElement(
|
|
2873
|
-
'
|
|
2874
|
-
{ className: 'cb-
|
|
2875
|
-
React.createElement(
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2897
|
+
'div',
|
|
2898
|
+
{ className: 'cb-footRow' },
|
|
2899
|
+
React.createElement(
|
|
2900
|
+
'div',
|
|
2901
|
+
{ className: 'cb-footHead' },
|
|
2902
|
+
React.createElement('span', { className: 'cb-footLabel' }, t('dashboard.rolling_window')),
|
|
2903
|
+
React.createElement('span', { className: 'cb-footAmount' }, `${100 - pct}% ${t('dashboard.remaining')}`)
|
|
2904
|
+
),
|
|
2905
|
+
React.createElement(
|
|
2906
|
+
'span',
|
|
2907
|
+
{ className: 'cb-footBar' },
|
|
2908
|
+
React.createElement('span', {
|
|
2909
|
+
className: isWarn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
|
|
2910
|
+
style: { width: `${Math.min(100, Math.max(2, pct))}%` },
|
|
2911
|
+
})
|
|
2912
|
+
)
|
|
2879
2913
|
)
|
|
2880
|
-
)
|
|
2914
|
+
),
|
|
2915
|
+
dialog
|
|
2881
2916
|
)
|
|
2882
2917
|
}
|
|
2883
2918
|
|
|
2884
|
-
// --- src/client/components/dashboard-
|
|
2885
|
-
|
|
2886
|
-
|
|
2919
|
+
// --- src/client/components/dashboard-dialog.js ---
|
|
2920
|
+
/**
|
|
2921
|
+
* The quota & status window, opened from the sidebar meter.
|
|
2922
|
+
*
|
|
2923
|
+
* This used to be a `main`-slot panel: clicking the meter called
|
|
2924
|
+
* `layout.selectPanel('clinebot-dashboard')`, which swapped the conversation out
|
|
2925
|
+
* of the centre column for a full-bleed dashboard. A status read-out has no
|
|
2926
|
+
* business replacing the conversation, so it is a window now — the shell's own
|
|
2927
|
+
* `Modal`, which brings the backdrop, the close affordance, Escape and focus
|
|
2928
|
+
* handling with it.
|
|
2929
|
+
*
|
|
2930
|
+
* `open` is owned by the caller (the meter), and the data is fetched on open
|
|
2931
|
+
* rather than on mount, so a closed window costs nothing.
|
|
2932
|
+
*/
|
|
2933
|
+
function DashboardDialog(props) {
|
|
2887
2934
|
const t = props?.t || makeT(en, en)
|
|
2935
|
+
const open = props?.open !== false
|
|
2936
|
+
const onClose = typeof props?.onClose === 'function' ? props.onClose : () => {}
|
|
2937
|
+
|
|
2888
2938
|
const [data, setData] = React.useState(null)
|
|
2889
2939
|
const [status, setStatus] = React.useState(null)
|
|
2890
2940
|
const [loading, setLoading] = React.useState(false)
|
|
2891
2941
|
const [updatedAt, setUpdatedAt] = React.useState('')
|
|
2892
2942
|
const [err, setErr] = React.useState('')
|
|
2893
2943
|
|
|
2894
|
-
const handleClose = () => {
|
|
2895
|
-
if (typeof props?.close === 'function') {
|
|
2896
|
-
props.close()
|
|
2897
|
-
} else {
|
|
2898
|
-
ctx?.get?.('layout')?.selectPanel?.(null)
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
2944
|
const loadData = React.useCallback(async () => {
|
|
2903
2945
|
setLoading(true)
|
|
2904
2946
|
setErr('')
|
|
@@ -2920,298 +2962,134 @@ function DashboardOverlay(props) {
|
|
|
2920
2962
|
}, [])
|
|
2921
2963
|
|
|
2922
2964
|
React.useEffect(() => {
|
|
2923
|
-
loadData()
|
|
2924
|
-
}, [loadData])
|
|
2965
|
+
if (open) loadData()
|
|
2966
|
+
}, [open, loadData])
|
|
2967
|
+
|
|
2968
|
+
if (!open) return null
|
|
2925
2969
|
|
|
2926
2970
|
const plan = data?.plan || status?.config?.defaultModel || 'ClinePass'
|
|
2927
2971
|
const email = data?.user?.email || ''
|
|
2928
2972
|
const activeAcc = status?.config?.activeAccount || 'Auto (Least-used)'
|
|
2929
2973
|
const enabledCount = status?.config?.enabledModels?.length || 0
|
|
2974
|
+
const routingMode = status?.config?.accountMode === 'round_robin' ? t('accounts.mode_round_robin') : t('accounts.mode_least_used')
|
|
2930
2975
|
|
|
2931
|
-
|
|
2976
|
+
const body = React.createElement(
|
|
2932
2977
|
'div',
|
|
2933
|
-
{ className: 'cb-
|
|
2978
|
+
{ className: 'cb-dialog' },
|
|
2934
2979
|
React.createElement(
|
|
2935
2980
|
'div',
|
|
2936
|
-
{ className: 'cb-
|
|
2937
|
-
|
|
2981
|
+
{ className: 'cb-dialogMeta' },
|
|
2982
|
+
updatedAt ? React.createElement('span', null, `${t('dashboard.updated_at')} ${updatedAt}`) : null,
|
|
2983
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
2938
2984
|
React.createElement(
|
|
2939
|
-
'
|
|
2940
|
-
{ className: 'cb-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
React.createElement('h2', { className: 'cb-mainTitle' }, t('dashboard.title')),
|
|
2945
|
-
React.createElement('p', { className: 'cb-mainSub' }, t('dashboard.subtitle'))
|
|
2946
|
-
),
|
|
2947
|
-
React.createElement('span', { style: { flex: 1 } }),
|
|
2948
|
-
updatedAt
|
|
2949
|
-
? React.createElement(
|
|
2950
|
-
'span',
|
|
2951
|
-
{ style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)', fontVariantNumeric: 'tabular-nums' } },
|
|
2952
|
-
`${t('dashboard.updated_at')} ${updatedAt}`
|
|
2953
|
-
)
|
|
2954
|
-
: null,
|
|
2955
|
-
React.createElement(
|
|
2956
|
-
'button',
|
|
2957
|
-
{
|
|
2958
|
-
type: 'button',
|
|
2959
|
-
className: 'cb-btn',
|
|
2960
|
-
disabled: loading,
|
|
2961
|
-
onClick: loadData,
|
|
2962
|
-
},
|
|
2963
|
-
loading ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
2964
|
-
),
|
|
2965
|
-
React.createElement(
|
|
2966
|
-
'button',
|
|
2967
|
-
{
|
|
2968
|
-
type: 'button',
|
|
2969
|
-
className: 'cb-btn',
|
|
2970
|
-
style: { minWidth: '32px', padding: '0 8px', fontSize: '16px' },
|
|
2971
|
-
'aria-label': t('dashboard.close'),
|
|
2972
|
-
title: t('dashboard.close'),
|
|
2973
|
-
onClick: handleClose,
|
|
2974
|
-
},
|
|
2975
|
-
'✕'
|
|
2976
|
-
)
|
|
2977
|
-
),
|
|
2985
|
+
'button',
|
|
2986
|
+
{ type: 'button', className: 'cb-btn', disabled: loading, onClick: loadData },
|
|
2987
|
+
loading ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
2988
|
+
)
|
|
2989
|
+
),
|
|
2978
2990
|
|
|
2979
|
-
|
|
2991
|
+
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
|
|
2980
2992
|
|
|
2981
|
-
|
|
2993
|
+
// Account and plan
|
|
2994
|
+
React.createElement(
|
|
2995
|
+
'div',
|
|
2996
|
+
{ className: 'cb-dialogCard' },
|
|
2982
2997
|
React.createElement(
|
|
2983
2998
|
'div',
|
|
2984
|
-
{
|
|
2999
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
2985
3000
|
React.createElement(
|
|
2986
3001
|
'div',
|
|
2987
|
-
{ style: { display: 'flex',
|
|
2988
|
-
React.createElement(
|
|
2989
|
-
|
|
2990
|
-
{ style: { display: 'flex', alignItems: 'center', gap: '10px' } },
|
|
2991
|
-
React.createElement('span', { style: { fontSize: '20px' } }, '💳'),
|
|
2992
|
-
React.createElement(
|
|
2993
|
-
'div',
|
|
2994
|
-
null,
|
|
2995
|
-
React.createElement('div', { style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, plan),
|
|
2996
|
-
email ? React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, email) : null
|
|
2997
|
-
)
|
|
2998
|
-
),
|
|
2999
|
-
React.createElement(
|
|
3000
|
-
'span',
|
|
3001
|
-
{ className: 'cb-badge cb-badge-ok' },
|
|
3002
|
-
t('badge.registered', { count: enabledCount })
|
|
3003
|
-
)
|
|
3002
|
+
{ style: { display: 'flex', flexDirection: 'column', minWidth: 0 } },
|
|
3003
|
+
React.createElement('div', { className: 'cb-row-title' }, plan),
|
|
3004
|
+
email ? React.createElement('div', { className: 'cb-row-desc' }, email) : null
|
|
3004
3005
|
),
|
|
3005
|
-
React.createElement(
|
|
3006
|
-
'div',
|
|
3007
|
-
{ className: 'cb-grid-2', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px', marginTop: '6px' } },
|
|
3008
|
-
React.createElement(
|
|
3009
|
-
'div',
|
|
3010
|
-
{ className: 'cb-stat-box' },
|
|
3011
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, t('dashboard.account_pool')),
|
|
3012
|
-
React.createElement('div', { className: 'cb-stat-val', style: { fontSize: '14px', wordBreak: 'break-all' } }, activeAcc)
|
|
3013
|
-
),
|
|
3014
|
-
React.createElement(
|
|
3015
|
-
'div',
|
|
3016
|
-
{ className: 'cb-stat-box' },
|
|
3017
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, t('accounts.mode_label')),
|
|
3018
|
-
React.createElement('div', { className: 'cb-stat-val', style: { fontSize: '14px' } }, status?.config?.accountMode === 'round_robin' ? t('accounts.mode_round_robin') : t('accounts.mode_least_used'))
|
|
3019
|
-
)
|
|
3020
|
-
)
|
|
3006
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('badge.registered', { count: enabledCount }))
|
|
3021
3007
|
),
|
|
3022
|
-
|
|
3023
|
-
// Card 2: Quota Windows
|
|
3024
3008
|
React.createElement(
|
|
3025
3009
|
'div',
|
|
3026
|
-
{ className: 'cb-
|
|
3010
|
+
{ className: 'cb-grid-2', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px' } },
|
|
3027
3011
|
React.createElement(
|
|
3028
3012
|
'div',
|
|
3029
|
-
{
|
|
3030
|
-
t('
|
|
3013
|
+
{ className: 'cb-stat-box' },
|
|
3014
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('dashboard.account_pool')),
|
|
3015
|
+
React.createElement('div', { className: 'cb-stat-val', style: { fontSize: '14px', wordBreak: 'break-all' } }, activeAcc)
|
|
3031
3016
|
),
|
|
3032
3017
|
React.createElement(
|
|
3033
3018
|
'div',
|
|
3034
|
-
{
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
label: t('quota.window_5h'),
|
|
3038
|
-
percentUsed: data.windows.fiveHour.percentUsed || 0,
|
|
3039
|
-
remainingPercent: data.windows.fiveHour.remainingPercent || 100,
|
|
3040
|
-
resetsAt: data.windows.fiveHour.resetsAt,
|
|
3041
|
-
t,
|
|
3042
|
-
})
|
|
3043
|
-
: null,
|
|
3044
|
-
React.createElement(ProgressBar, {
|
|
3045
|
-
label: t('quota.window_weekly'),
|
|
3046
|
-
percentUsed: data?.windows?.weekly?.percentUsed || 0,
|
|
3047
|
-
remainingPercent: data?.windows?.weekly?.remainingPercent || 100,
|
|
3048
|
-
resetsAt: data?.windows?.weekly?.resetsAt,
|
|
3049
|
-
t,
|
|
3050
|
-
}),
|
|
3051
|
-
data?.windows?.monthly
|
|
3052
|
-
? React.createElement(ProgressBar, {
|
|
3053
|
-
label: t('quota.window_monthly'),
|
|
3054
|
-
percentUsed: data.windows.monthly.percentUsed || 0,
|
|
3055
|
-
remainingPercent: data.windows.monthly.remainingPercent || 100,
|
|
3056
|
-
resetsAt: data.windows.monthly.resetsAt,
|
|
3057
|
-
t,
|
|
3058
|
-
})
|
|
3059
|
-
: null
|
|
3019
|
+
{ className: 'cb-stat-box' },
|
|
3020
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('accounts.mode_label')),
|
|
3021
|
+
React.createElement('div', { className: 'cb-stat-val', style: { fontSize: '14px' } }, routingMode)
|
|
3060
3022
|
)
|
|
3061
3023
|
)
|
|
3062
|
-
)
|
|
3063
|
-
)
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
// --- src/client/components/models-provider-card.js ---
|
|
3067
|
-
function ModelsProviderCard(props) {
|
|
3068
|
-
const ctx = props?.ctx
|
|
3069
|
-
const t = props?.t || makeT(en, en)
|
|
3070
|
-
const [status, setStatus] = React.useState(null)
|
|
3071
|
-
const [apiKey, setApiKey] = React.useState('')
|
|
3072
|
-
const [showKey, setShowKey] = React.useState(false)
|
|
3073
|
-
const [saving, setSaving] = React.useState(false)
|
|
3074
|
-
const [msg, setMsg] = React.useState('')
|
|
3075
|
-
const [err, setErr] = React.useState('')
|
|
3076
|
-
|
|
3077
|
-
const loadStatus = React.useCallback(async () => {
|
|
3078
|
-
try {
|
|
3079
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
|
|
3080
|
-
if (!res.ok) return
|
|
3081
|
-
const json = await res.json().catch(() => null)
|
|
3082
|
-
if (json && json.ok) setStatus(json)
|
|
3083
|
-
} catch {
|
|
3084
|
-
// quiet on network fail
|
|
3085
|
-
}
|
|
3086
|
-
}, [])
|
|
3087
|
-
|
|
3088
|
-
React.useEffect(() => {
|
|
3089
|
-
loadStatus()
|
|
3090
|
-
}, [loadStatus])
|
|
3091
|
-
|
|
3092
|
-
const keyConfigured = Boolean(status?.keyPresent || status?.config?.hasKey)
|
|
3093
|
-
const modelsCount = status?.config?.enabledModels?.length || status?.availableModels?.length || 0
|
|
3094
|
-
|
|
3095
|
-
const handleSaveKey = async () => {
|
|
3096
|
-
const keyVal = String(apiKey || '').trim()
|
|
3097
|
-
if (!keyVal) {
|
|
3098
|
-
setErr(t('key.empty_err'))
|
|
3099
|
-
return
|
|
3100
|
-
}
|
|
3101
|
-
setSaving(true)
|
|
3102
|
-
setErr('')
|
|
3103
|
-
setMsg('')
|
|
3104
|
-
try {
|
|
3105
|
-
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/save-key`, {
|
|
3106
|
-
method: 'POST',
|
|
3107
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3108
|
-
body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: status?.config?.apiKeyEnv }),
|
|
3109
|
-
})
|
|
3110
|
-
const data = await res.json().catch(() => ({}))
|
|
3111
|
-
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3112
|
-
setApiKey('')
|
|
3113
|
-
setMsg(t('card.key_saved'))
|
|
3114
|
-
await loadStatus()
|
|
3115
|
-
} catch (e) {
|
|
3116
|
-
setErr(String(e?.message || e))
|
|
3117
|
-
} finally {
|
|
3118
|
-
setSaving(false)
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
const handleOpenSettings = () => {
|
|
3123
|
-
const layout = ctx?.get?.('layout')
|
|
3124
|
-
if (typeof layout?.selectPanel === 'function') {
|
|
3125
|
-
layout.selectPanel('clinebot-dashboard')
|
|
3126
|
-
} else if (typeof props?.openSettings === 'function') {
|
|
3127
|
-
props.openSettings()
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3024
|
+
),
|
|
3130
3025
|
|
|
3131
|
-
|
|
3132
|
-
'div',
|
|
3133
|
-
{ className: 'cb-providerCard', 'data-clinebot-provider-card': 'true' },
|
|
3134
|
-
// Header
|
|
3026
|
+
// Quota windows
|
|
3135
3027
|
React.createElement(
|
|
3136
3028
|
'div',
|
|
3137
|
-
{
|
|
3029
|
+
{ className: 'cb-dialogCard' },
|
|
3030
|
+
React.createElement('div', { className: 'cb-section-title' }, t('quota.title')),
|
|
3138
3031
|
React.createElement(
|
|
3139
3032
|
'div',
|
|
3140
|
-
{ style: { display: 'flex',
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3033
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
3034
|
+
data?.windows?.fiveHour
|
|
3035
|
+
? React.createElement(ProgressBar, {
|
|
3036
|
+
label: t('quota.window_5h'),
|
|
3037
|
+
percentUsed: data.windows.fiveHour.percentUsed || 0,
|
|
3038
|
+
remainingPercent: data.windows.fiveHour.remainingPercent || 100,
|
|
3039
|
+
resetsAt: data.windows.fiveHour.resetsAt,
|
|
3040
|
+
t,
|
|
3041
|
+
})
|
|
3042
|
+
: null,
|
|
3043
|
+
React.createElement(ProgressBar, {
|
|
3044
|
+
label: t('quota.window_weekly'),
|
|
3045
|
+
percentUsed: data?.windows?.weekly?.percentUsed || 0,
|
|
3046
|
+
remainingPercent: data?.windows?.weekly?.remainingPercent || 100,
|
|
3047
|
+
resetsAt: data?.windows?.weekly?.resetsAt,
|
|
3048
|
+
t,
|
|
3049
|
+
}),
|
|
3050
|
+
data?.windows?.monthly
|
|
3051
|
+
? React.createElement(ProgressBar, {
|
|
3052
|
+
label: t('quota.window_monthly'),
|
|
3053
|
+
percentUsed: data.windows.monthly.percentUsed || 0,
|
|
3054
|
+
remainingPercent: data.windows.monthly.remainingPercent || 100,
|
|
3055
|
+
resetsAt: data.windows.monthly.resetsAt,
|
|
3056
|
+
t,
|
|
3057
|
+
})
|
|
3158
3058
|
: null
|
|
3159
3059
|
)
|
|
3160
|
-
)
|
|
3060
|
+
)
|
|
3061
|
+
)
|
|
3161
3062
|
|
|
3162
|
-
|
|
3063
|
+
if (Modal) {
|
|
3064
|
+
return React.createElement(Modal, {
|
|
3065
|
+
open: true,
|
|
3066
|
+
onClose,
|
|
3067
|
+
title: t('dashboard.title'),
|
|
3068
|
+
description: t('dashboard.subtitle'),
|
|
3069
|
+
closeLabel: t('dashboard.close'),
|
|
3070
|
+
children: body,
|
|
3071
|
+
})
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// Older shells without the primitive still get a window, just a plainer one.
|
|
3075
|
+
return React.createElement(
|
|
3076
|
+
'div',
|
|
3077
|
+
{ className: 'cb-dialogFallback', role: 'dialog', 'aria-label': t('dashboard.title'), onClick: onClose },
|
|
3163
3078
|
React.createElement(
|
|
3164
3079
|
'div',
|
|
3165
|
-
{
|
|
3080
|
+
{ className: 'cb-dialogFallbackPanel', onClick: (e) => e.stopPropagation() },
|
|
3166
3081
|
React.createElement(
|
|
3167
3082
|
'div',
|
|
3168
|
-
{ className: 'cb-
|
|
3169
|
-
React.createElement('
|
|
3170
|
-
|
|
3171
|
-
className: 'cb-input',
|
|
3172
|
-
style: { flex: 1 },
|
|
3173
|
-
value: apiKey,
|
|
3174
|
-
placeholder: keyConfigured ? t('key.placeholder_has') : t('key.placeholder_empty'),
|
|
3175
|
-
onChange: (e) => setApiKey(e.target.value),
|
|
3176
|
-
onKeyDown: (e) => { if (e.key === 'Enter') handleSaveKey() },
|
|
3177
|
-
}),
|
|
3178
|
-
React.createElement(
|
|
3179
|
-
'button',
|
|
3180
|
-
{
|
|
3181
|
-
type: 'button',
|
|
3182
|
-
className: 'cb-btn',
|
|
3183
|
-
onClick: () => setShowKey(!showKey),
|
|
3184
|
-
},
|
|
3185
|
-
showKey ? t('key.hide') : t('key.show')
|
|
3186
|
-
),
|
|
3083
|
+
{ className: 'cb-dialogMeta' },
|
|
3084
|
+
React.createElement('span', { className: 'cb-row-title' }, t('dashboard.title')),
|
|
3085
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
3187
3086
|
React.createElement(
|
|
3188
3087
|
'button',
|
|
3189
|
-
{
|
|
3190
|
-
|
|
3191
|
-
className: 'cb-btn cb-btn-primary',
|
|
3192
|
-
disabled: saving || !apiKey.trim(),
|
|
3193
|
-
onClick: handleSaveKey,
|
|
3194
|
-
},
|
|
3195
|
-
saving ? t('key.saving') : t('card.save_key')
|
|
3088
|
+
{ type: 'button', className: 'cb-btn', 'aria-label': t('dashboard.close'), onClick: onClose },
|
|
3089
|
+
t('dashboard.close')
|
|
3196
3090
|
)
|
|
3197
3091
|
),
|
|
3198
|
-
|
|
3199
|
-
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null
|
|
3200
|
-
),
|
|
3201
|
-
|
|
3202
|
-
// Footer with navigation to full settings
|
|
3203
|
-
React.createElement(
|
|
3204
|
-
'div',
|
|
3205
|
-
{ style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '10px' } },
|
|
3206
|
-
React.createElement(
|
|
3207
|
-
'button',
|
|
3208
|
-
{
|
|
3209
|
-
type: 'button',
|
|
3210
|
-
className: 'cb-btn',
|
|
3211
|
-
onClick: handleOpenSettings,
|
|
3212
|
-
},
|
|
3213
|
-
`⚙️ ${t('card.open_settings')}`
|
|
3214
|
-
)
|
|
3092
|
+
body
|
|
3215
3093
|
)
|
|
3216
3094
|
)
|
|
3217
3095
|
}
|
|
@@ -3379,7 +3257,7 @@ function ComposerCostDock(props) {
|
|
|
3379
3257
|
function SettingsPage(props) {
|
|
3380
3258
|
const ctx = props?.ctx
|
|
3381
3259
|
const t = props?.t || makeT(en, en)
|
|
3382
|
-
const
|
|
3260
|
+
const configSnapshot = useConfigFormsSnapshot(ctx)
|
|
3383
3261
|
|
|
3384
3262
|
const [status, setStatus] = React.useState(null)
|
|
3385
3263
|
const [draft, setDraft] = React.useState(null)
|
|
@@ -3764,7 +3642,7 @@ function SettingsPage(props) {
|
|
|
3764
3642
|
React.createElement(
|
|
3765
3643
|
'div',
|
|
3766
3644
|
{ className: 'cb-page-title' },
|
|
3767
|
-
|
|
3645
|
+
t('header.title'),
|
|
3768
3646
|
React.createElement(
|
|
3769
3647
|
'span',
|
|
3770
3648
|
{ className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
|
|
@@ -3788,7 +3666,7 @@ function SettingsPage(props) {
|
|
|
3788
3666
|
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
|
|
3789
3667
|
|
|
3790
3668
|
// Settings host status warning
|
|
3791
|
-
|
|
3669
|
+
configSnapshot?.status === 'unavailable'
|
|
3792
3670
|
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, t('settings.unavailable'))
|
|
3793
3671
|
: null,
|
|
3794
3672
|
|
|
@@ -3870,65 +3748,6 @@ function SettingsPage(props) {
|
|
|
3870
3748
|
)
|
|
3871
3749
|
}
|
|
3872
3750
|
|
|
3873
|
-
// --- src/client/plugin-card.js ---
|
|
3874
|
-
function PluginCard(props) {
|
|
3875
|
-
const page = !!(props && props.view === 'page')
|
|
3876
|
-
const [open, setOpen] = React.useState(!!page)
|
|
3877
|
-
const t = props?.t || makeT(en, en)
|
|
3878
|
-
React.useEffect(() => {
|
|
3879
|
-
ensureCss()
|
|
3880
|
-
}, [])
|
|
3881
|
-
|
|
3882
|
-
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
3883
|
-
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
3884
|
-
if (props && props.view === 'summary') {
|
|
3885
|
-
return React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
3886
|
-
}
|
|
3887
|
-
|
|
3888
|
-
return React.createElement(
|
|
3889
|
-
page ? 'div' : 'li',
|
|
3890
|
-
{ className: page ? 'cb-page' : 'cb-section-card', style: page ? undefined : { listStyle: 'none', marginBottom: '12px' } },
|
|
3891
|
-
React.createElement(
|
|
3892
|
-
'button',
|
|
3893
|
-
{
|
|
3894
|
-
type: 'button',
|
|
3895
|
-
style: {
|
|
3896
|
-
background: 'none',
|
|
3897
|
-
border: 'none',
|
|
3898
|
-
cursor: 'pointer',
|
|
3899
|
-
display: page ? 'none' : 'flex',
|
|
3900
|
-
alignItems: 'center',
|
|
3901
|
-
width: '100%',
|
|
3902
|
-
padding: 0,
|
|
3903
|
-
textAlign: 'left',
|
|
3904
|
-
},
|
|
3905
|
-
'aria-expanded': page ? true : open,
|
|
3906
|
-
onClick: () => setOpen((v) => !v),
|
|
3907
|
-
},
|
|
3908
|
-
React.createElement(
|
|
3909
|
-
'div',
|
|
3910
|
-
{ style: { flex: 1 } },
|
|
3911
|
-
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
|
|
3912
|
-
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
3913
|
-
),
|
|
3914
|
-
React.createElement('span', { className: 'cb-chevron' + (open ? ' cb-chevron-open' : '') },
|
|
3915
|
-
React.createElement(Chevron)
|
|
3916
|
-
)
|
|
3917
|
-
),
|
|
3918
|
-
(page || open)
|
|
3919
|
-
? React.createElement(
|
|
3920
|
-
'div',
|
|
3921
|
-
{ style: { marginTop: '16px' } },
|
|
3922
|
-
React.createElement(
|
|
3923
|
-
ErrorBoundary,
|
|
3924
|
-
{ t },
|
|
3925
|
-
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
3926
|
-
)
|
|
3927
|
-
)
|
|
3928
|
-
: null
|
|
3929
|
-
)
|
|
3930
|
-
}
|
|
3931
|
-
|
|
3932
3751
|
// --- src/client/entry.js ---
|
|
3933
3752
|
function ensureSettingsForm(ctx) {
|
|
3934
3753
|
const svc = readConfigForms(ctx)
|
|
@@ -3941,6 +3760,12 @@ function ensureSettingsForm(ctx) {
|
|
|
3941
3760
|
}
|
|
3942
3761
|
|
|
3943
3762
|
function apply(ctx) {
|
|
3763
|
+
// The stylesheet has to be in the document before any of this plugin's slots
|
|
3764
|
+
// render. `sidebar.footer.action` and `main` mount on boot, so leaving the
|
|
3765
|
+
// injection to the settings page's mount effect renders those seats unstyled
|
|
3766
|
+
// until the user opens the plugin page.
|
|
3767
|
+
ensureCss()
|
|
3768
|
+
|
|
3944
3769
|
const addLocale = (locale, dictionary) => {
|
|
3945
3770
|
try {
|
|
3946
3771
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -3993,50 +3818,31 @@ function apply(ctx) {
|
|
|
3993
3818
|
}
|
|
3994
3819
|
}
|
|
3995
3820
|
|
|
3996
|
-
// The
|
|
3997
|
-
//
|
|
3998
|
-
//
|
|
3999
|
-
//
|
|
4000
|
-
|
|
3821
|
+
// The plugin's configuration is a section of DSH Settings (`settings.section`),
|
|
3822
|
+
// a peer of General/Models/Plugins — not a row in the Plugins list and not a tab
|
|
3823
|
+
// inside the Plugins settings section. The shell owns the modal, the section nav
|
|
3824
|
+
// and the scrolling body, and hands a section `close` as its one affordance; the
|
|
3825
|
+
// section draws its own content. An unregistered id gets the shell's default
|
|
3826
|
+
// settings glyph in the nav.
|
|
3827
|
+
registerSlotWhenReady('settings.section', () =>
|
|
4001
3828
|
ctx.slots.register(
|
|
4002
3829
|
{
|
|
4003
|
-
name: '
|
|
3830
|
+
name: 'settings.section',
|
|
4004
3831
|
id: NS,
|
|
4005
|
-
order:
|
|
4006
|
-
label: () =>
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
// The core keys the seat as `<bundle name>#<row id>`; `bundle.name` may be the
|
|
4016
|
-
// package name or the short bundle name depending on the manager's view, so both
|
|
4017
|
-
// spellings are registered — the unused one is inert.
|
|
4018
|
-
// Row seat: key = ROW_CONFIG_KEY ('@goodandready/dsh-clinebot#dsh-clinebot').
|
|
4019
|
-
registerSlotWhenReady('plugins.row.config', () =>
|
|
4020
|
-
ctx.slots.register(
|
|
4021
|
-
{
|
|
4022
|
-
name: 'plugins.row.config',
|
|
4023
|
-
key: ROW_CONFIG_KEY,
|
|
4024
|
-
locale: NS,
|
|
4025
|
-
inject: () => ({ ctx }),
|
|
4026
|
-
},
|
|
4027
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
4028
|
-
)
|
|
4029
|
-
)
|
|
4030
|
-
|
|
4031
|
-
registerSlotWhenReady('settings.plugin.item', () =>
|
|
4032
|
-
ctx.slots.register(
|
|
4033
|
-
{
|
|
4034
|
-
name: 'settings.plugin.item',
|
|
4035
|
-
key: NS,
|
|
3832
|
+
order: 30,
|
|
3833
|
+
label: () => {
|
|
3834
|
+
try {
|
|
3835
|
+
const t = ctx.locale && typeof ctx.locale.bind === 'function' ? ctx.locale.bind(NS) : null
|
|
3836
|
+
const label = t ? t('nav') : ''
|
|
3837
|
+
return label && label !== 'nav' ? label : 'ClineBot'
|
|
3838
|
+
} catch (_) {
|
|
3839
|
+
return 'ClineBot'
|
|
3840
|
+
}
|
|
3841
|
+
},
|
|
4036
3842
|
locale: NS,
|
|
4037
3843
|
inject: () => ({ ctx }),
|
|
4038
3844
|
},
|
|
4039
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(
|
|
3845
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
4040
3846
|
)
|
|
4041
3847
|
)
|
|
4042
3848
|
|
|
@@ -4054,33 +3860,18 @@ function apply(ctx) {
|
|
|
4054
3860
|
)
|
|
4055
3861
|
)
|
|
4056
3862
|
|
|
4057
|
-
//
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
// Settings Models Provider Card (settings.models.provider-card slot)
|
|
4071
|
-
for (const pKey of ['clinebot', 'llm-clinebot']) {
|
|
4072
|
-
registerSlotWhenReady('settings.models.provider-card', () =>
|
|
4073
|
-
ctx.slots.register(
|
|
4074
|
-
{
|
|
4075
|
-
name: 'settings.models.provider-card',
|
|
4076
|
-
key: pKey,
|
|
4077
|
-
locale: NS,
|
|
4078
|
-
inject: () => ({ ctx }),
|
|
4079
|
-
},
|
|
4080
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(ModelsProviderCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
4081
|
-
)
|
|
4082
|
-
)
|
|
4083
|
-
}
|
|
3863
|
+
// The quota & status read-out is a window opened from the sidebar meter, not a
|
|
3864
|
+
// `main`-slot panel: occupying the centre column meant clicking the meter threw
|
|
3865
|
+
// the conversation out of the way to show a status read-out.
|
|
3866
|
+
|
|
3867
|
+
// `settings.models.provider-card` is deliberately not registered. The seat is
|
|
3868
|
+
// KEYED and dispatched with `entryKey = <provider settingsNs>` on every provider
|
|
3869
|
+
// row, unconditionally: the models page renders it before the row's own
|
|
3870
|
+
// `open ? editor : null`, and its owner props carry no open state. Anything
|
|
3871
|
+
// registered there therefore shows on the collapsed row and reads as an editor
|
|
3872
|
+
// that opened by itself. The row's 编辑 affordance already opens a provider
|
|
3873
|
+
// editor with the same API-key field, and this plugin's configuration lives in
|
|
3874
|
+
// its own Settings section, so the seat is left free.
|
|
4084
3875
|
|
|
4085
3876
|
// Composer Session Cost Dock (conversation.composer.dock slot)
|
|
4086
3877
|
registerSlotWhenReady('conversation.composer.dock', () =>
|