@goodandready/dsh-clinebot 0.5.28 → 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 +20 -0
- package/lib/client.js +629 -586
- package/lib/config.js +9 -0
- package/lib/index.js +31 -0
- package/lib/key-discovery.js +109 -0
- package/lib/multimodal-budget.js +108 -0
- package/lib/proxy-token.js +2 -2
- package/lib/routes/accounts.js +17 -0
- 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,16 +236,20 @@ const en = {
|
|
|
234
236
|
'dashboard.account_pool': 'Active Routing Account',
|
|
235
237
|
'dashboard.credits': 'Credit Balance',
|
|
236
238
|
'dashboard.updated_at': 'Updated at',
|
|
237
|
-
'
|
|
238
|
-
'
|
|
239
|
-
'
|
|
240
|
-
'
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
'
|
|
239
|
+
'composer.cost_title': 'Estimated Session Cost',
|
|
240
|
+
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
241
|
+
'composer.prompt_tok': 'Prompt Tokens',
|
|
242
|
+
'composer.completion_tok': 'Completion Tokens',
|
|
243
|
+
'composer.reasoning_tok': 'Reasoning Tokens',
|
|
244
|
+
'composer.est_cost': 'Estimated Cost',
|
|
245
|
+
'discovery.title': 'Discovered Local Keys',
|
|
246
|
+
'discovery.import_btn': 'Import from {source}',
|
|
247
|
+
'discovery.imported': 'Imported to accounts pool',
|
|
248
|
+
'discovery.scanning': 'Scanning for local Cline keys…',
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
const zh = {
|
|
252
|
+
nav: 'ClineBot',
|
|
247
253
|
title: 'ClineBot',
|
|
248
254
|
subtitle: 'ClinePass 订阅服务,包含动态套餐模型。',
|
|
249
255
|
'settings.loading': '正在加载 ClineBot 设置…',
|
|
@@ -258,7 +264,7 @@ const en = {
|
|
|
258
264
|
'badge.key_missing': '未配置密钥',
|
|
259
265
|
'badge.registered': '已接入 DSH ({count} 个模型)',
|
|
260
266
|
'badge.not_registered': '未接入 DSH',
|
|
261
|
-
'key.title': '
|
|
267
|
+
'key.title': '授权与 API 密钥',
|
|
262
268
|
'key.desc': '密钥直接加密保存在 DSH 凭据管理器 (~/.dsh/.credentials.yaml),绝不会以明文形式暴露在普通配置文件中。',
|
|
263
269
|
'key.placeholder_has': '••••••••••••••••••••••••',
|
|
264
270
|
'key.placeholder_empty': '粘贴 ClinePass 密钥 (cline_...)',
|
|
@@ -266,7 +272,7 @@ const en = {
|
|
|
266
272
|
'key.hide': '隐藏',
|
|
267
273
|
'key.save': '保存密钥',
|
|
268
274
|
'key.saving': '正在保存…',
|
|
269
|
-
'key.login_fast': '
|
|
275
|
+
'key.login_fast': '打开 API 密钥页 ↗',
|
|
270
276
|
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
271
277
|
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
272
278
|
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
@@ -274,7 +280,7 @@ const en = {
|
|
|
274
280
|
'key.get_key': '前往 app.cline.bot 控制台获取密钥 ↗',
|
|
275
281
|
'key.saved_msg': '密钥已成功保存至 DSH 凭据服务。验证状态:{status}',
|
|
276
282
|
'key.empty_err': '保存前请输入 API 密钥',
|
|
277
|
-
'accounts.title': '
|
|
283
|
+
'accounts.title': '多账号故障转移与轮换池 (Failover)',
|
|
278
284
|
'accounts.desc': '添加备用账号。当某个账号触发 429 速率限制或配额耗尽时,ClineBot 会智能无缝切换至剩余额度最高的备用账号。',
|
|
279
285
|
'accounts.active_badge': '当前活动',
|
|
280
286
|
'accounts.pinned_badge': '固定使用',
|
|
@@ -296,17 +302,17 @@ const en = {
|
|
|
296
302
|
'accounts.route_model_placeholder': '模型 ID (例如 cline-pass/deepseek-v4-pro)',
|
|
297
303
|
'accounts.route_account_placeholder': '目标账号',
|
|
298
304
|
'accounts.route_btn_add': '添加路由',
|
|
299
|
-
'quota.title': '
|
|
305
|
+
'quota.title': 'ClinePass 用量配额与限额监控',
|
|
300
306
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
301
307
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
302
308
|
'quota.refresh': '刷新配额',
|
|
303
309
|
'quota.refreshing': '正在刷新…',
|
|
304
310
|
'quota.refreshed_msg': '订阅用量配额已刷新',
|
|
305
311
|
'quota.window_5h': '⏱ 5 小时滚动限额',
|
|
306
|
-
'quota.window_weekly': '
|
|
312
|
+
'quota.window_weekly': '每周滚动窗口',
|
|
307
313
|
'quota.used': '已使用 {pct}%',
|
|
308
314
|
'quota.reset_at': '重置时间:{time}',
|
|
309
|
-
'quota.window_monthly': '
|
|
315
|
+
'quota.window_monthly': '每月滚动窗口',
|
|
310
316
|
'quota.countdown': '{time}后重置',
|
|
311
317
|
'quota.resetting': '正在重置',
|
|
312
318
|
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
@@ -314,7 +320,7 @@ const en = {
|
|
|
314
320
|
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
315
321
|
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
316
322
|
'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
317
|
-
'models.reasoning_tag': '
|
|
323
|
+
'models.reasoning_tag': '深度思考',
|
|
318
324
|
'models.effort_low': '低',
|
|
319
325
|
'models.effort_medium': '中',
|
|
320
326
|
'models.effort_high': '高',
|
|
@@ -338,10 +344,10 @@ const en = {
|
|
|
338
344
|
'models.show_all': '显示全部',
|
|
339
345
|
'models.clean_stale': '清理过期 ({count})',
|
|
340
346
|
'models.models_selected': '已选择 {selected} / {total} 个模型用于聊天',
|
|
341
|
-
'models.title': '
|
|
347
|
+
'models.title': 'DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
342
348
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
343
349
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
344
|
-
'models.sync': '
|
|
350
|
+
'models.sync': '同步订阅模型',
|
|
345
351
|
'models.syncing': '正在同步…',
|
|
346
352
|
'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
|
|
347
353
|
'models.verified': '已与套餐同步',
|
|
@@ -367,7 +373,7 @@ const en = {
|
|
|
367
373
|
'models.btn_probe': '测试延迟',
|
|
368
374
|
'models.star': '推荐',
|
|
369
375
|
'models.auto_new': '新上线',
|
|
370
|
-
'models.apply_original_all': '
|
|
376
|
+
'models.apply_original_all': '原厂规格',
|
|
371
377
|
'models.reset_contexts_all': '↺ 重置上下文',
|
|
372
378
|
'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
|
|
373
379
|
'models.ctx_default': '默认 ({ctx} / {max})',
|
|
@@ -384,7 +390,7 @@ const en = {
|
|
|
384
390
|
'models.custom_max_label': '最大输出',
|
|
385
391
|
'models.ctx_tag_original': '原厂',
|
|
386
392
|
'models.ctx_tag_custom': '自定义',
|
|
387
|
-
'stats.title': '
|
|
393
|
+
'stats.title': '当前 DSH 会话请求遥测',
|
|
388
394
|
'stats.desc': '本地实时监控请求次数、网络延迟以及通过 ClinePass 消耗的估算 Token 数量。',
|
|
389
395
|
'stats.requests': '成功 / 总请求次数',
|
|
390
396
|
'stats.tokens': '会话累计 Token 估算',
|
|
@@ -403,7 +409,7 @@ const en = {
|
|
|
403
409
|
'stats.reset_btn': '重置统计',
|
|
404
410
|
'stats.confirm_reset': '确认重置?',
|
|
405
411
|
'stats.cancel_reset': '取消',
|
|
406
|
-
'diag.title': '
|
|
412
|
+
'diag.title': '诊断工具与服务接入',
|
|
407
413
|
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试或强制重新同步。',
|
|
408
414
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
409
415
|
'diag.smoke_testing': '正在测试…',
|
|
@@ -439,13 +445,16 @@ const en = {
|
|
|
439
445
|
'dashboard.account_pool': '当前承载账户',
|
|
440
446
|
'dashboard.credits': '点数余额',
|
|
441
447
|
'dashboard.updated_at': '更新于',
|
|
442
|
-
'
|
|
443
|
-
'
|
|
444
|
-
'
|
|
445
|
-
'
|
|
446
|
-
'
|
|
447
|
-
'
|
|
448
|
-
'
|
|
448
|
+
'composer.cost_title': '预估会话成本',
|
|
449
|
+
'composer.cost_breakdown': 'Token 与费用明细',
|
|
450
|
+
'composer.prompt_tok': '提示词 Token',
|
|
451
|
+
'composer.completion_tok': '补全 Token',
|
|
452
|
+
'composer.reasoning_tok': '思考推理 Token',
|
|
453
|
+
'composer.est_cost': '预估花费',
|
|
454
|
+
'discovery.title': '发现本地密钥',
|
|
455
|
+
'discovery.import_btn': '从 {source} 导入',
|
|
456
|
+
'discovery.imported': '已成功导入账户池',
|
|
457
|
+
'discovery.scanning': '正在扫描本地 Cline 密钥…',
|
|
449
458
|
}
|
|
450
459
|
|
|
451
460
|
function makeT(dict, fallback) {
|
|
@@ -463,28 +472,50 @@ const en = {
|
|
|
463
472
|
}
|
|
464
473
|
}
|
|
465
474
|
|
|
466
|
-
// --- src/client/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
stroke: 'currentColor',
|
|
474
|
-
strokeWidth: 1.5,
|
|
475
|
-
strokeLinecap: 'round',
|
|
476
|
-
strokeLinejoin: 'round',
|
|
477
|
-
})
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
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
|
|
481
482
|
try {
|
|
482
|
-
|
|
483
|
-
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
|
|
483
|
+
primitives = require('@deepseek-ai/dsh-client-ui-primitives') || null
|
|
484
484
|
} catch (_) {
|
|
485
|
-
|
|
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
|
+
})
|
|
486
518
|
}
|
|
487
|
-
const Chevron = ChevronIcon || FallbackChevron
|
|
488
519
|
|
|
489
520
|
// --- src/client/styles.js ---
|
|
490
521
|
function ensureCss() {
|
|
@@ -494,88 +525,97 @@ function ensureCss() {
|
|
|
494
525
|
style.id = 'dsh-clinebot-full-css'
|
|
495
526
|
style.dataset.dshPlugin = NS
|
|
496
527
|
style.textContent = `
|
|
497
|
-
.cb-page{display:flex;flex-direction:column;gap:
|
|
498
|
-
.cb-
|
|
499
|
-
.cb-
|
|
500
|
-
.cb-
|
|
501
|
-
|
|
502
|
-
.cb-
|
|
503
|
-
|
|
504
|
-
.cb-section-
|
|
505
|
-
|
|
506
|
-
.cb-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.cb-
|
|
510
|
-
|
|
511
|
-
.cb-badge{
|
|
512
|
-
.cb-badge-
|
|
513
|
-
|
|
514
|
-
.cb-
|
|
515
|
-
|
|
516
|
-
.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)}
|
|
517
547
|
.cb-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}
|
|
518
|
-
.cb-input-group{display:flex;
|
|
519
|
-
|
|
520
|
-
.cb-
|
|
521
|
-
|
|
522
|
-
.cb-btn-
|
|
523
|
-
.cb-btn
|
|
524
|
-
.cb-btn-
|
|
525
|
-
.cb-btn
|
|
526
|
-
.cb-btn-
|
|
527
|
-
|
|
528
|
-
.cb-
|
|
529
|
-
.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}
|
|
530
564
|
.cb-progress-label{font-weight:500;white-space:nowrap}
|
|
531
|
-
.cb-progress-meta, .cb-bar-meta{
|
|
532
|
-
.cb-progress-track, .cb-bar-track{width:100%;height:
|
|
533
|
-
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:
|
|
534
|
-
.cb-progress-bar-warn{height:100%;border-radius:
|
|
535
|
-
.cb-progress-bar-bad{height:100%;border-radius:
|
|
536
|
-
|
|
537
|
-
.cb-table{width:100%;border-collapse:collapse;margin-top:
|
|
538
|
-
.cb-table th{
|
|
539
|
-
.cb-table td{padding:10px;border-bottom
|
|
540
|
-
.cb-table tr:hover{background:var(--dsw-alias-bg-
|
|
541
|
-
|
|
542
|
-
.cb-alert
|
|
543
|
-
.cb-alert-
|
|
544
|
-
.cb-alert-
|
|
545
|
-
.cb-
|
|
546
|
-
.cb-banner-
|
|
547
|
-
.cb-
|
|
548
|
-
.cb-stat-
|
|
549
|
-
.cb-stat-
|
|
550
|
-
.cb-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
.
|
|
556
|
-
|
|
557
|
-
.
|
|
558
|
-
.cb-
|
|
559
|
-
.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}
|
|
560
600
|
.cb-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
561
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}
|
|
562
|
-
.cb-footBar{display:block;background:var(--dsw-alias-bg-layer-2);
|
|
563
|
-
.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}
|
|
564
604
|
.cb-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
565
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}
|
|
566
|
-
.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}
|
|
567
607
|
.cb-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
568
|
-
.cb-railButton:focus-visible{outline:
|
|
569
|
-
.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)}
|
|
570
610
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
.cb-
|
|
574
|
-
.cb-
|
|
575
|
-
.cb-
|
|
576
|
-
.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)}
|
|
577
618
|
|
|
578
|
-
.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}
|
|
579
619
|
`
|
|
580
620
|
document.head.appendChild(style)
|
|
581
621
|
}
|
|
@@ -950,6 +990,40 @@ function AccountsSection({
|
|
|
950
990
|
const [deleteSecret, setDeleteSecret] = React.useState(true)
|
|
951
991
|
const [routeModel, setRouteModel] = React.useState('')
|
|
952
992
|
const [routeAccount, setRouteAccount] = React.useState('')
|
|
993
|
+
const [discoveredKeys, setDiscoveredKeys] = React.useState([])
|
|
994
|
+
|
|
995
|
+
React.useEffect(() => {
|
|
996
|
+
let active = true
|
|
997
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/keys/discovered`, { cache: 'no-store' })
|
|
998
|
+
.then((r) => r.json())
|
|
999
|
+
.then((data) => {
|
|
1000
|
+
if (active && data?.ok && Array.isArray(data.discovered)) {
|
|
1001
|
+
setDiscoveredKeys(data.discovered)
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
.catch(() => {})
|
|
1005
|
+
return () => { active = false }
|
|
1006
|
+
}, [])
|
|
1007
|
+
|
|
1008
|
+
async function handleImportDiscovered(item) {
|
|
1009
|
+
let max = 1
|
|
1010
|
+
for (const a of (status.accounts || [])) {
|
|
1011
|
+
const m = String(a.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
1012
|
+
if (m) {
|
|
1013
|
+
const n = parseInt(m[1], 10)
|
|
1014
|
+
if (n >= max) max = n + 1
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const targetEnv = `CLINEBOT_API_KEY_${max}`
|
|
1018
|
+
if (typeof handleAddAccount === 'function') {
|
|
1019
|
+
await handleAddAccount({
|
|
1020
|
+
label: item.source || targetEnv,
|
|
1021
|
+
apiKeyEnv: targetEnv,
|
|
1022
|
+
apiKey: item.key,
|
|
1023
|
+
})
|
|
1024
|
+
setDiscoveredKeys((prev) => prev.filter((k) => k.key !== item.key))
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
953
1027
|
|
|
954
1028
|
function handleOpenAdd() {
|
|
955
1029
|
let max = 1
|
|
@@ -1011,6 +1085,31 @@ function AccountsSection({
|
|
|
1011
1085
|
: null
|
|
1012
1086
|
),
|
|
1013
1087
|
React.createElement('div', { className: 'cb-section-desc' }, t('accounts.desc')),
|
|
1088
|
+
discoveredKeys.length > 0
|
|
1089
|
+
? React.createElement(
|
|
1090
|
+
'div',
|
|
1091
|
+
{ className: 'cb-alert-ok', style: { display: 'flex', flexDirection: 'column', gap: '6px', margin: '6px 0' } },
|
|
1092
|
+
React.createElement('div', { style: { fontWeight: 600 } }, `🔍 ${t('discovery.title')}`),
|
|
1093
|
+
discoveredKeys.map((item, idx) =>
|
|
1094
|
+
React.createElement(
|
|
1095
|
+
'div',
|
|
1096
|
+
{ key: idx, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1097
|
+
React.createElement('span', null, `${item.maskedKey} (${item.source})`),
|
|
1098
|
+
React.createElement(
|
|
1099
|
+
'button',
|
|
1100
|
+
{
|
|
1101
|
+
type: 'button',
|
|
1102
|
+
className: 'cb-btn',
|
|
1103
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
1104
|
+
disabled: !!busy,
|
|
1105
|
+
onClick: () => handleImportDiscovered(item),
|
|
1106
|
+
},
|
|
1107
|
+
t('discovery.import_btn', { source: item.source })
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
)
|
|
1111
|
+
)
|
|
1112
|
+
: null,
|
|
1014
1113
|
React.createElement(
|
|
1015
1114
|
'div',
|
|
1016
1115
|
{
|
|
@@ -1454,18 +1553,17 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, sho
|
|
|
1454
1553
|
),
|
|
1455
1554
|
React.createElement(
|
|
1456
1555
|
'div',
|
|
1457
|
-
{ 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)' } },
|
|
1458
1557
|
React.createElement(
|
|
1459
1558
|
'div',
|
|
1460
|
-
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1461
|
-
React.createElement('span', { style: { fontSize: '
|
|
1462
|
-
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'))
|
|
1463
1562
|
),
|
|
1464
|
-
React.createElement(
|
|
1465
|
-
type: 'checkbox',
|
|
1466
|
-
style: { cursor: 'pointer', width: '16px', height: '16px' },
|
|
1563
|
+
React.createElement(BooleanControl, {
|
|
1467
1564
|
checked: showSidebarQuota !== false,
|
|
1468
|
-
|
|
1565
|
+
label: t('sidebar.show_quota'),
|
|
1566
|
+
onChange: (next) => onToggleSidebarQuota && onToggleSidebarQuota(next),
|
|
1469
1567
|
})
|
|
1470
1568
|
)
|
|
1471
1569
|
)
|
|
@@ -1735,17 +1833,21 @@ function VisibleModelsSub({
|
|
|
1735
1833
|
},
|
|
1736
1834
|
React.createElement(
|
|
1737
1835
|
'div',
|
|
1738
|
-
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '
|
|
1836
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' } },
|
|
1739
1837
|
React.createElement(
|
|
1740
|
-
'
|
|
1741
|
-
{ style: { display: 'flex', alignItems: 'center', gap: '
|
|
1742
|
-
React.createElement(
|
|
1743
|
-
|
|
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, {
|
|
1744
1846
|
checked: !!filterModelsByPlan,
|
|
1745
1847
|
disabled: !!busy,
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1848
|
+
label: t('models.filter_by_plan'),
|
|
1849
|
+
onChange: (next) => onConfigUpdate && onConfigUpdate({ filterModelsByPlan: next }),
|
|
1850
|
+
})
|
|
1749
1851
|
),
|
|
1750
1852
|
React.createElement(
|
|
1751
1853
|
'div',
|
|
@@ -2589,25 +2691,25 @@ function DiagSection({
|
|
|
2589
2691
|
)
|
|
2590
2692
|
),
|
|
2591
2693
|
React.createElement(
|
|
2592
|
-
'
|
|
2593
|
-
{ style: { display: '
|
|
2594
|
-
React.createElement('input', {
|
|
2595
|
-
type: 'checkbox',
|
|
2596
|
-
checked: !!proxyMode,
|
|
2597
|
-
disabled: !!busy,
|
|
2598
|
-
onChange: (e) => onToggleProxy && onToggleProxy(e.target.checked),
|
|
2599
|
-
}),
|
|
2694
|
+
'div',
|
|
2695
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px' } },
|
|
2600
2696
|
React.createElement(
|
|
2601
2697
|
'span',
|
|
2602
2698
|
{
|
|
2603
2699
|
style: {
|
|
2604
|
-
fontSize: '
|
|
2605
|
-
|
|
2700
|
+
fontSize: '14px',
|
|
2701
|
+
lineHeight: '20px',
|
|
2606
2702
|
color: proxyMode ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
2607
2703
|
},
|
|
2608
2704
|
},
|
|
2609
2705
|
proxyMode ? t('diag.proxy_enabled') : t('diag.proxy_disabled')
|
|
2610
|
-
)
|
|
2706
|
+
),
|
|
2707
|
+
React.createElement(BooleanControl, {
|
|
2708
|
+
checked: !!proxyMode,
|
|
2709
|
+
disabled: !!busy,
|
|
2710
|
+
label: t('diag.proxy_title'),
|
|
2711
|
+
onChange: (next) => onToggleProxy && onToggleProxy(next),
|
|
2712
|
+
})
|
|
2611
2713
|
)
|
|
2612
2714
|
),
|
|
2613
2715
|
React.createElement(
|
|
@@ -2700,10 +2802,14 @@ function MiniRing({ percent, size = 18, warn = false }) {
|
|
|
2700
2802
|
)
|
|
2701
2803
|
}
|
|
2702
2804
|
|
|
2703
|
-
function SidebarQuota({ ctx, wide,
|
|
2805
|
+
function SidebarQuota({ ctx, wide, t: propT }) {
|
|
2704
2806
|
const t = propT || makeT(en, en)
|
|
2705
2807
|
const [data, setData] = React.useState(null)
|
|
2706
|
-
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
|
|
2707
2813
|
|
|
2708
2814
|
const loadQuota = React.useCallback(async () => {
|
|
2709
2815
|
try {
|
|
@@ -2716,7 +2822,9 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2716
2822
|
}
|
|
2717
2823
|
}, [])
|
|
2718
2824
|
|
|
2825
|
+
// Poll only while the meter is on screen — a hidden meter has nothing to refresh.
|
|
2719
2826
|
React.useEffect(() => {
|
|
2827
|
+
if (!show) return undefined
|
|
2720
2828
|
let timer = null
|
|
2721
2829
|
const scheduleNext = () => {
|
|
2722
2830
|
timer = setTimeout(async () => {
|
|
@@ -2727,12 +2835,9 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2727
2835
|
loadQuota()
|
|
2728
2836
|
scheduleNext()
|
|
2729
2837
|
return () => { if (timer) clearTimeout(timer) }
|
|
2730
|
-
}, [loadQuota])
|
|
2838
|
+
}, [loadQuota, show])
|
|
2731
2839
|
|
|
2732
|
-
|
|
2733
|
-
const config = useConfigFormsSnapshot(ctx)
|
|
2734
|
-
const show = config?.showSidebarQuota !== false
|
|
2735
|
-
if (!show || !visible) return null
|
|
2840
|
+
if (!show) return null
|
|
2736
2841
|
|
|
2737
2842
|
const fiveHour = data?.windows?.fiveHour
|
|
2738
2843
|
const weekly = data?.windows?.weekly
|
|
@@ -2741,85 +2846,101 @@ function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
|
2741
2846
|
const isWarn = pct >= 85
|
|
2742
2847
|
const planName = data?.plan || t('title')
|
|
2743
2848
|
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
} else {
|
|
2748
|
-
ctx?.get?.('layout')?.selectPanel?.('clinebot-dashboard')
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
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)
|
|
2751
2852
|
|
|
2752
2853
|
const tooltip = `${t('sidebar.quota_title')}: ${pct}% ${t('dashboard.used')} (${planName})`
|
|
2854
|
+
const dialog = React.createElement(DashboardDialog, { t, open, onClose: () => setOpen(false) })
|
|
2753
2855
|
|
|
2754
2856
|
if (!wide) {
|
|
2755
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(
|
|
2756
2880
|
'button',
|
|
2757
2881
|
{
|
|
2758
2882
|
type: 'button',
|
|
2759
|
-
className: 'cb-
|
|
2883
|
+
className: 'cb-foot',
|
|
2760
2884
|
'aria-label': tooltip,
|
|
2885
|
+
'aria-expanded': open,
|
|
2761
2886
|
title: tooltip,
|
|
2762
2887
|
onClick: handleOpen,
|
|
2763
2888
|
},
|
|
2764
|
-
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 20 })
|
|
2765
|
-
)
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
return React.createElement(
|
|
2769
|
-
'button',
|
|
2770
|
-
{
|
|
2771
|
-
type: 'button',
|
|
2772
|
-
className: 'cb-foot',
|
|
2773
|
-
'aria-label': tooltip,
|
|
2774
|
-
title: tooltip,
|
|
2775
|
-
onClick: handleOpen,
|
|
2776
|
-
},
|
|
2777
|
-
React.createElement(
|
|
2778
|
-
'div',
|
|
2779
|
-
{ className: 'cb-footTop' },
|
|
2780
|
-
React.createElement(MiniRing, { percent: pct, warn: isWarn, size: 16 }),
|
|
2781
|
-
React.createElement('span', { className: 'cb-footName' }, planName),
|
|
2782
|
-
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}%`)
|
|
2783
|
-
),
|
|
2784
|
-
React.createElement(
|
|
2785
|
-
'div',
|
|
2786
|
-
{ className: 'cb-footRow' },
|
|
2787
2889
|
React.createElement(
|
|
2788
2890
|
'div',
|
|
2789
|
-
{ className: 'cb-
|
|
2790
|
-
React.createElement(
|
|
2791
|
-
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}%`)
|
|
2792
2895
|
),
|
|
2793
2896
|
React.createElement(
|
|
2794
|
-
'
|
|
2795
|
-
{ className: 'cb-
|
|
2796
|
-
React.createElement(
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
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
|
+
)
|
|
2800
2913
|
)
|
|
2801
|
-
)
|
|
2914
|
+
),
|
|
2915
|
+
dialog
|
|
2802
2916
|
)
|
|
2803
2917
|
}
|
|
2804
2918
|
|
|
2805
|
-
// --- src/client/components/dashboard-
|
|
2806
|
-
|
|
2807
|
-
|
|
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) {
|
|
2808
2934
|
const t = props?.t || makeT(en, en)
|
|
2935
|
+
const open = props?.open !== false
|
|
2936
|
+
const onClose = typeof props?.onClose === 'function' ? props.onClose : () => {}
|
|
2937
|
+
|
|
2809
2938
|
const [data, setData] = React.useState(null)
|
|
2810
2939
|
const [status, setStatus] = React.useState(null)
|
|
2811
2940
|
const [loading, setLoading] = React.useState(false)
|
|
2812
2941
|
const [updatedAt, setUpdatedAt] = React.useState('')
|
|
2813
2942
|
const [err, setErr] = React.useState('')
|
|
2814
2943
|
|
|
2815
|
-
const handleClose = () => {
|
|
2816
|
-
if (typeof props?.close === 'function') {
|
|
2817
|
-
props.close()
|
|
2818
|
-
} else {
|
|
2819
|
-
ctx?.get?.('layout')?.selectPanel?.(null)
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
2944
|
const loadData = React.useCallback(async () => {
|
|
2824
2945
|
setLoading(true)
|
|
2825
2946
|
setErr('')
|
|
@@ -2841,299 +2962,294 @@ function DashboardOverlay(props) {
|
|
|
2841
2962
|
}, [])
|
|
2842
2963
|
|
|
2843
2964
|
React.useEffect(() => {
|
|
2844
|
-
loadData()
|
|
2845
|
-
}, [loadData])
|
|
2965
|
+
if (open) loadData()
|
|
2966
|
+
}, [open, loadData])
|
|
2967
|
+
|
|
2968
|
+
if (!open) return null
|
|
2846
2969
|
|
|
2847
2970
|
const plan = data?.plan || status?.config?.defaultModel || 'ClinePass'
|
|
2848
2971
|
const email = data?.user?.email || ''
|
|
2849
2972
|
const activeAcc = status?.config?.activeAccount || 'Auto (Least-used)'
|
|
2850
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')
|
|
2851
2975
|
|
|
2852
|
-
|
|
2976
|
+
const body = React.createElement(
|
|
2853
2977
|
'div',
|
|
2854
|
-
{ className: 'cb-
|
|
2978
|
+
{ className: 'cb-dialog' },
|
|
2855
2979
|
React.createElement(
|
|
2856
2980
|
'div',
|
|
2857
|
-
{ className: 'cb-
|
|
2858
|
-
|
|
2981
|
+
{ className: 'cb-dialogMeta' },
|
|
2982
|
+
updatedAt ? React.createElement('span', null, `${t('dashboard.updated_at')} ${updatedAt}`) : null,
|
|
2983
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
2859
2984
|
React.createElement(
|
|
2860
|
-
'
|
|
2861
|
-
{ className: 'cb-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
React.createElement('h2', { className: 'cb-mainTitle' }, t('dashboard.title')),
|
|
2866
|
-
React.createElement('p', { className: 'cb-mainSub' }, t('dashboard.subtitle'))
|
|
2867
|
-
),
|
|
2868
|
-
React.createElement('span', { style: { flex: 1 } }),
|
|
2869
|
-
updatedAt
|
|
2870
|
-
? React.createElement(
|
|
2871
|
-
'span',
|
|
2872
|
-
{ style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)', fontVariantNumeric: 'tabular-nums' } },
|
|
2873
|
-
`${t('dashboard.updated_at')} ${updatedAt}`
|
|
2874
|
-
)
|
|
2875
|
-
: null,
|
|
2876
|
-
React.createElement(
|
|
2877
|
-
'button',
|
|
2878
|
-
{
|
|
2879
|
-
type: 'button',
|
|
2880
|
-
className: 'cb-btn',
|
|
2881
|
-
disabled: loading,
|
|
2882
|
-
onClick: loadData,
|
|
2883
|
-
},
|
|
2884
|
-
loading ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
2885
|
-
),
|
|
2886
|
-
React.createElement(
|
|
2887
|
-
'button',
|
|
2888
|
-
{
|
|
2889
|
-
type: 'button',
|
|
2890
|
-
className: 'cb-btn',
|
|
2891
|
-
style: { minWidth: '32px', padding: '0 8px', fontSize: '16px' },
|
|
2892
|
-
'aria-label': t('dashboard.close'),
|
|
2893
|
-
title: t('dashboard.close'),
|
|
2894
|
-
onClick: handleClose,
|
|
2895
|
-
},
|
|
2896
|
-
'✕'
|
|
2897
|
-
)
|
|
2898
|
-
),
|
|
2985
|
+
'button',
|
|
2986
|
+
{ type: 'button', className: 'cb-btn', disabled: loading, onClick: loadData },
|
|
2987
|
+
loading ? t('dashboard.refreshing') : t('dashboard.refresh')
|
|
2988
|
+
)
|
|
2989
|
+
),
|
|
2899
2990
|
|
|
2900
|
-
|
|
2991
|
+
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
|
|
2901
2992
|
|
|
2902
|
-
|
|
2993
|
+
// Account and plan
|
|
2994
|
+
React.createElement(
|
|
2995
|
+
'div',
|
|
2996
|
+
{ className: 'cb-dialogCard' },
|
|
2903
2997
|
React.createElement(
|
|
2904
2998
|
'div',
|
|
2905
|
-
{
|
|
2999
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
2906
3000
|
React.createElement(
|
|
2907
3001
|
'div',
|
|
2908
|
-
{ style: { display: 'flex',
|
|
2909
|
-
React.createElement(
|
|
2910
|
-
|
|
2911
|
-
{ style: { display: 'flex', alignItems: 'center', gap: '10px' } },
|
|
2912
|
-
React.createElement('span', { style: { fontSize: '20px' } }, '💳'),
|
|
2913
|
-
React.createElement(
|
|
2914
|
-
'div',
|
|
2915
|
-
null,
|
|
2916
|
-
React.createElement('div', { style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, plan),
|
|
2917
|
-
email ? React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, email) : null
|
|
2918
|
-
)
|
|
2919
|
-
),
|
|
2920
|
-
React.createElement(
|
|
2921
|
-
'span',
|
|
2922
|
-
{ className: 'cb-badge cb-badge-ok' },
|
|
2923
|
-
t('badge.registered', { count: enabledCount })
|
|
2924
|
-
)
|
|
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
|
|
2925
3005
|
),
|
|
2926
|
-
React.createElement(
|
|
2927
|
-
'div',
|
|
2928
|
-
{ className: 'cb-grid-2', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px', marginTop: '6px' } },
|
|
2929
|
-
React.createElement(
|
|
2930
|
-
'div',
|
|
2931
|
-
{ className: 'cb-stat-box' },
|
|
2932
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, t('dashboard.account_pool')),
|
|
2933
|
-
React.createElement('div', { className: 'cb-stat-val', style: { fontSize: '14px', wordBreak: 'break-all' } }, activeAcc)
|
|
2934
|
-
),
|
|
2935
|
-
React.createElement(
|
|
2936
|
-
'div',
|
|
2937
|
-
{ className: 'cb-stat-box' },
|
|
2938
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, t('accounts.mode_label')),
|
|
2939
|
-
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'))
|
|
2940
|
-
)
|
|
2941
|
-
)
|
|
3006
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, t('badge.registered', { count: enabledCount }))
|
|
2942
3007
|
),
|
|
2943
|
-
|
|
2944
|
-
// Card 2: Quota Windows
|
|
2945
3008
|
React.createElement(
|
|
2946
3009
|
'div',
|
|
2947
|
-
{ className: 'cb-
|
|
3010
|
+
{ className: 'cb-grid-2', style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px' } },
|
|
2948
3011
|
React.createElement(
|
|
2949
3012
|
'div',
|
|
2950
|
-
{
|
|
2951
|
-
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)
|
|
2952
3016
|
),
|
|
2953
3017
|
React.createElement(
|
|
2954
3018
|
'div',
|
|
2955
|
-
{
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
label: t('quota.window_5h'),
|
|
2959
|
-
percentUsed: data.windows.fiveHour.percentUsed || 0,
|
|
2960
|
-
remainingPercent: data.windows.fiveHour.remainingPercent || 100,
|
|
2961
|
-
resetsAt: data.windows.fiveHour.resetsAt,
|
|
2962
|
-
t,
|
|
2963
|
-
})
|
|
2964
|
-
: null,
|
|
2965
|
-
React.createElement(ProgressBar, {
|
|
2966
|
-
label: t('quota.window_weekly'),
|
|
2967
|
-
percentUsed: data?.windows?.weekly?.percentUsed || 0,
|
|
2968
|
-
remainingPercent: data?.windows?.weekly?.remainingPercent || 100,
|
|
2969
|
-
resetsAt: data?.windows?.weekly?.resetsAt,
|
|
2970
|
-
t,
|
|
2971
|
-
}),
|
|
2972
|
-
data?.windows?.monthly
|
|
2973
|
-
? React.createElement(ProgressBar, {
|
|
2974
|
-
label: t('quota.window_monthly'),
|
|
2975
|
-
percentUsed: data.windows.monthly.percentUsed || 0,
|
|
2976
|
-
remainingPercent: data.windows.monthly.remainingPercent || 100,
|
|
2977
|
-
resetsAt: data.windows.monthly.resetsAt,
|
|
2978
|
-
t,
|
|
2979
|
-
})
|
|
2980
|
-
: 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)
|
|
2981
3022
|
)
|
|
2982
3023
|
)
|
|
3024
|
+
),
|
|
3025
|
+
|
|
3026
|
+
// Quota windows
|
|
3027
|
+
React.createElement(
|
|
3028
|
+
'div',
|
|
3029
|
+
{ className: 'cb-dialogCard' },
|
|
3030
|
+
React.createElement('div', { className: 'cb-section-title' }, t('quota.title')),
|
|
3031
|
+
React.createElement(
|
|
3032
|
+
'div',
|
|
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
|
+
})
|
|
3058
|
+
: null
|
|
3059
|
+
)
|
|
3060
|
+
)
|
|
3061
|
+
)
|
|
3062
|
+
|
|
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 },
|
|
3078
|
+
React.createElement(
|
|
3079
|
+
'div',
|
|
3080
|
+
{ className: 'cb-dialogFallbackPanel', onClick: (e) => e.stopPropagation() },
|
|
3081
|
+
React.createElement(
|
|
3082
|
+
'div',
|
|
3083
|
+
{ className: 'cb-dialogMeta' },
|
|
3084
|
+
React.createElement('span', { className: 'cb-row-title' }, t('dashboard.title')),
|
|
3085
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
3086
|
+
React.createElement(
|
|
3087
|
+
'button',
|
|
3088
|
+
{ type: 'button', className: 'cb-btn', 'aria-label': t('dashboard.close'), onClick: onClose },
|
|
3089
|
+
t('dashboard.close')
|
|
3090
|
+
)
|
|
3091
|
+
),
|
|
3092
|
+
body
|
|
2983
3093
|
)
|
|
2984
3094
|
)
|
|
2985
3095
|
}
|
|
2986
3096
|
|
|
2987
|
-
// --- src/client/components/
|
|
2988
|
-
function
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
const [saving, setSaving] = React.useState(false)
|
|
2995
|
-
const [msg, setMsg] = React.useState('')
|
|
2996
|
-
const [err, setErr] = React.useState('')
|
|
3097
|
+
// --- src/client/components/composer-cost.js ---
|
|
3098
|
+
function formatTokens(count) {
|
|
3099
|
+
const n = Number(count) || 0
|
|
3100
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
|
|
3101
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
|
|
3102
|
+
return String(n)
|
|
3103
|
+
}
|
|
2997
3104
|
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
} catch {
|
|
3005
|
-
// quiet on network fail
|
|
3006
|
-
}
|
|
3007
|
-
}, [])
|
|
3105
|
+
function calculateCost(tokens, modelId = '') {
|
|
3106
|
+
const isPremium = String(modelId).includes('claude') || String(modelId).includes('pro')
|
|
3107
|
+
// Rates per 1M tokens
|
|
3108
|
+
const promptRate = isPremium ? 3.0 : 0.15
|
|
3109
|
+
const completionRate = isPremium ? 15.0 : 0.60
|
|
3110
|
+
const reasoningRate = isPremium ? 15.0 : 0.60
|
|
3008
3111
|
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3112
|
+
const pTok = Number(tokens?.promptTokens || tokens?.inputTokens || 0)
|
|
3113
|
+
const cTok = Number(tokens?.completionTokens || tokens?.outputTokens || 0)
|
|
3114
|
+
const rTok = Number(tokens?.reasoningTokens || 0)
|
|
3012
3115
|
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3116
|
+
const cost = (pTok * promptRate + cTok * completionRate + rTok * reasoningRate) / 1000000
|
|
3117
|
+
return { cost, pTok, cTok, rTok, total: pTok + cTok + rTok }
|
|
3118
|
+
}
|
|
3015
3119
|
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
setSaving(false)
|
|
3120
|
+
function ComposerCostDock(props) {
|
|
3121
|
+
const t = props?.t || makeT(en, en)
|
|
3122
|
+
const [stats, setStats] = React.useState(null)
|
|
3123
|
+
const [showPopover, setShowPopover] = React.useState(false)
|
|
3124
|
+
|
|
3125
|
+
// Use projection hook if passed by DSH composer dock, or poll session stats
|
|
3126
|
+
const projectedTokens = typeof props?.useProjection === 'function' ? props.useProjection('tokenUsage') : null
|
|
3127
|
+
const projectedModel = typeof props?.useProjection === 'function' ? props.useProjection('modelSelection') : null
|
|
3128
|
+
|
|
3129
|
+
React.useEffect(() => {
|
|
3130
|
+
if (projectedTokens) return
|
|
3131
|
+
let active = true
|
|
3132
|
+
const loadStats = async () => {
|
|
3133
|
+
try {
|
|
3134
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/stats`, { cache: 'no-store' })
|
|
3135
|
+
if (!res.ok) return
|
|
3136
|
+
const data = await res.json().catch(() => null)
|
|
3137
|
+
if (active && data?.stats?.summary) {
|
|
3138
|
+
setStats(data.stats.summary)
|
|
3139
|
+
}
|
|
3140
|
+
} catch {
|
|
3141
|
+
// quiet
|
|
3142
|
+
}
|
|
3040
3143
|
}
|
|
3144
|
+
loadStats()
|
|
3145
|
+
const timer = setTimeout(loadStats, 30000)
|
|
3146
|
+
return () => { active = false; clearTimeout(timer) }
|
|
3147
|
+
}, [projectedTokens])
|
|
3148
|
+
|
|
3149
|
+
const tokens = projectedTokens || {
|
|
3150
|
+
promptTokens: stats?.promptTokens || 0,
|
|
3151
|
+
completionTokens: stats?.completionTokens || 0,
|
|
3152
|
+
reasoningTokens: stats?.reasoningTokens || 0,
|
|
3041
3153
|
}
|
|
3042
3154
|
|
|
3043
|
-
const
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
props.openSettings()
|
|
3049
|
-
}
|
|
3155
|
+
const modelId = projectedModel?.id || ''
|
|
3156
|
+
const { cost, pTok, cTok, rTok, total } = calculateCost(tokens, modelId)
|
|
3157
|
+
|
|
3158
|
+
if (total === 0 && !stats) {
|
|
3159
|
+
return null
|
|
3050
3160
|
}
|
|
3051
3161
|
|
|
3162
|
+
const costDisplay = cost < 0.001 && cost > 0 ? '< $0.001' : `$${cost.toFixed(3)}`
|
|
3163
|
+
|
|
3052
3164
|
return React.createElement(
|
|
3053
3165
|
'div',
|
|
3054
|
-
{
|
|
3055
|
-
|
|
3166
|
+
{
|
|
3167
|
+
className: 'cb-composer-cost-dock',
|
|
3168
|
+
'data-composer-session-cost': 'true',
|
|
3169
|
+
style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
|
|
3170
|
+
},
|
|
3056
3171
|
React.createElement(
|
|
3057
|
-
'
|
|
3058
|
-
{
|
|
3059
|
-
|
|
3060
|
-
'
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
'
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
)
|
|
3172
|
+
'button',
|
|
3173
|
+
{
|
|
3174
|
+
type: 'button',
|
|
3175
|
+
className: 'cb-btn',
|
|
3176
|
+
style: {
|
|
3177
|
+
padding: '2px 8px',
|
|
3178
|
+
fontSize: '11px',
|
|
3179
|
+
height: '24px',
|
|
3180
|
+
borderRadius: '999px',
|
|
3181
|
+
display: 'inline-flex',
|
|
3182
|
+
alignItems: 'center',
|
|
3183
|
+
gap: '4px',
|
|
3184
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
3185
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
3186
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3187
|
+
cursor: 'pointer',
|
|
3188
|
+
},
|
|
3189
|
+
title: `${t('composer.cost_title')}: ${costDisplay} (${formatTokens(total)} tokens)`,
|
|
3190
|
+
onClick: () => setShowPopover(!showPopover),
|
|
3191
|
+
},
|
|
3192
|
+
React.createElement('span', { style: { fontSize: '12px' } }, '🪙'),
|
|
3193
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `≈ ${costDisplay}`),
|
|
3194
|
+
React.createElement('span', { style: { opacity: 0.7 } }, `· ${formatTokens(total)} tok`)
|
|
3081
3195
|
),
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
'div',
|
|
3086
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
3087
|
-
React.createElement(
|
|
3088
|
-
'div',
|
|
3089
|
-
{ className: 'cb-input-group' },
|
|
3090
|
-
React.createElement('input', {
|
|
3091
|
-
type: showKey ? 'text' : 'password',
|
|
3092
|
-
className: 'cb-input',
|
|
3093
|
-
style: { flex: 1 },
|
|
3094
|
-
value: apiKey,
|
|
3095
|
-
placeholder: keyConfigured ? t('key.placeholder_has') : t('key.placeholder_empty'),
|
|
3096
|
-
onChange: (e) => setApiKey(e.target.value),
|
|
3097
|
-
onKeyDown: (e) => { if (e.key === 'Enter') handleSaveKey() },
|
|
3098
|
-
}),
|
|
3099
|
-
React.createElement(
|
|
3100
|
-
'button',
|
|
3101
|
-
{
|
|
3102
|
-
type: 'button',
|
|
3103
|
-
className: 'cb-btn',
|
|
3104
|
-
onClick: () => setShowKey(!showKey),
|
|
3105
|
-
},
|
|
3106
|
-
showKey ? t('key.hide') : t('key.show')
|
|
3107
|
-
),
|
|
3108
|
-
React.createElement(
|
|
3109
|
-
'button',
|
|
3196
|
+
showPopover
|
|
3197
|
+
? React.createElement(
|
|
3198
|
+
'div',
|
|
3110
3199
|
{
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3200
|
+
className: 'cb-popover',
|
|
3201
|
+
style: {
|
|
3202
|
+
position: 'absolute',
|
|
3203
|
+
bottom: '100%',
|
|
3204
|
+
left: 0,
|
|
3205
|
+
marginBottom: '6px',
|
|
3206
|
+
minWidth: '200px',
|
|
3207
|
+
padding: '10px 12px',
|
|
3208
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
3209
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3210
|
+
borderRadius: '8px',
|
|
3211
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
3212
|
+
zIndex: 100,
|
|
3213
|
+
fontSize: '12px',
|
|
3214
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
3215
|
+
display: 'flex',
|
|
3216
|
+
flexDirection: 'column',
|
|
3217
|
+
gap: '6px',
|
|
3218
|
+
},
|
|
3115
3219
|
},
|
|
3116
|
-
|
|
3220
|
+
React.createElement(
|
|
3221
|
+
'div',
|
|
3222
|
+
{ style: { fontWeight: 600, borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '4px' } },
|
|
3223
|
+
t('composer.cost_breakdown')
|
|
3224
|
+
),
|
|
3225
|
+
React.createElement(
|
|
3226
|
+
'div',
|
|
3227
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3228
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.prompt_tok')),
|
|
3229
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(pTok))
|
|
3230
|
+
),
|
|
3231
|
+
React.createElement(
|
|
3232
|
+
'div',
|
|
3233
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3234
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.completion_tok')),
|
|
3235
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(cTok))
|
|
3236
|
+
),
|
|
3237
|
+
rTok > 0
|
|
3238
|
+
? React.createElement(
|
|
3239
|
+
'div',
|
|
3240
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3241
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.reasoning_tok')),
|
|
3242
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(rTok))
|
|
3243
|
+
)
|
|
3244
|
+
: null,
|
|
3245
|
+
React.createElement(
|
|
3246
|
+
'div',
|
|
3247
|
+
{ style: { display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '4px', fontWeight: 600 } },
|
|
3248
|
+
React.createElement('span', null, t('composer.est_cost')),
|
|
3249
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-brand-primary)' } }, `≈ ${costDisplay}`)
|
|
3250
|
+
)
|
|
3117
3251
|
)
|
|
3118
|
-
|
|
3119
|
-
msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
|
|
3120
|
-
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null
|
|
3121
|
-
),
|
|
3122
|
-
|
|
3123
|
-
// Footer with navigation to full settings
|
|
3124
|
-
React.createElement(
|
|
3125
|
-
'div',
|
|
3126
|
-
{ style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '10px' } },
|
|
3127
|
-
React.createElement(
|
|
3128
|
-
'button',
|
|
3129
|
-
{
|
|
3130
|
-
type: 'button',
|
|
3131
|
-
className: 'cb-btn',
|
|
3132
|
-
onClick: handleOpenSettings,
|
|
3133
|
-
},
|
|
3134
|
-
`⚙️ ${t('card.open_settings')}`
|
|
3135
|
-
)
|
|
3136
|
-
)
|
|
3252
|
+
: null
|
|
3137
3253
|
)
|
|
3138
3254
|
}
|
|
3139
3255
|
|
|
@@ -3141,7 +3257,7 @@ function ModelsProviderCard(props) {
|
|
|
3141
3257
|
function SettingsPage(props) {
|
|
3142
3258
|
const ctx = props?.ctx
|
|
3143
3259
|
const t = props?.t || makeT(en, en)
|
|
3144
|
-
const
|
|
3260
|
+
const configSnapshot = useConfigFormsSnapshot(ctx)
|
|
3145
3261
|
|
|
3146
3262
|
const [status, setStatus] = React.useState(null)
|
|
3147
3263
|
const [draft, setDraft] = React.useState(null)
|
|
@@ -3526,7 +3642,7 @@ function SettingsPage(props) {
|
|
|
3526
3642
|
React.createElement(
|
|
3527
3643
|
'div',
|
|
3528
3644
|
{ className: 'cb-page-title' },
|
|
3529
|
-
|
|
3645
|
+
t('header.title'),
|
|
3530
3646
|
React.createElement(
|
|
3531
3647
|
'span',
|
|
3532
3648
|
{ className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
|
|
@@ -3550,7 +3666,7 @@ function SettingsPage(props) {
|
|
|
3550
3666
|
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
|
|
3551
3667
|
|
|
3552
3668
|
// Settings host status warning
|
|
3553
|
-
|
|
3669
|
+
configSnapshot?.status === 'unavailable'
|
|
3554
3670
|
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, t('settings.unavailable'))
|
|
3555
3671
|
: null,
|
|
3556
3672
|
|
|
@@ -3632,65 +3748,6 @@ function SettingsPage(props) {
|
|
|
3632
3748
|
)
|
|
3633
3749
|
}
|
|
3634
3750
|
|
|
3635
|
-
// --- src/client/plugin-card.js ---
|
|
3636
|
-
function PluginCard(props) {
|
|
3637
|
-
const page = !!(props && props.view === 'page')
|
|
3638
|
-
const [open, setOpen] = React.useState(!!page)
|
|
3639
|
-
const t = props?.t || makeT(en, en)
|
|
3640
|
-
React.useEffect(() => {
|
|
3641
|
-
ensureCss()
|
|
3642
|
-
}, [])
|
|
3643
|
-
|
|
3644
|
-
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
3645
|
-
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
3646
|
-
if (props && props.view === 'summary') {
|
|
3647
|
-
return React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
return React.createElement(
|
|
3651
|
-
page ? 'div' : 'li',
|
|
3652
|
-
{ className: page ? 'cb-page' : 'cb-section-card', style: page ? undefined : { listStyle: 'none', marginBottom: '12px' } },
|
|
3653
|
-
React.createElement(
|
|
3654
|
-
'button',
|
|
3655
|
-
{
|
|
3656
|
-
type: 'button',
|
|
3657
|
-
style: {
|
|
3658
|
-
background: 'none',
|
|
3659
|
-
border: 'none',
|
|
3660
|
-
cursor: 'pointer',
|
|
3661
|
-
display: page ? 'none' : 'flex',
|
|
3662
|
-
alignItems: 'center',
|
|
3663
|
-
width: '100%',
|
|
3664
|
-
padding: 0,
|
|
3665
|
-
textAlign: 'left',
|
|
3666
|
-
},
|
|
3667
|
-
'aria-expanded': page ? true : open,
|
|
3668
|
-
onClick: () => setOpen((v) => !v),
|
|
3669
|
-
},
|
|
3670
|
-
React.createElement(
|
|
3671
|
-
'div',
|
|
3672
|
-
{ style: { flex: 1 } },
|
|
3673
|
-
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
|
|
3674
|
-
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
3675
|
-
),
|
|
3676
|
-
React.createElement('span', { className: 'cb-chevron' + (open ? ' cb-chevron-open' : '') },
|
|
3677
|
-
React.createElement(Chevron)
|
|
3678
|
-
)
|
|
3679
|
-
),
|
|
3680
|
-
(page || open)
|
|
3681
|
-
? React.createElement(
|
|
3682
|
-
'div',
|
|
3683
|
-
{ style: { marginTop: '16px' } },
|
|
3684
|
-
React.createElement(
|
|
3685
|
-
ErrorBoundary,
|
|
3686
|
-
{ t },
|
|
3687
|
-
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
3688
|
-
)
|
|
3689
|
-
)
|
|
3690
|
-
: null
|
|
3691
|
-
)
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
3751
|
// --- src/client/entry.js ---
|
|
3695
3752
|
function ensureSettingsForm(ctx) {
|
|
3696
3753
|
const svc = readConfigForms(ctx)
|
|
@@ -3703,6 +3760,12 @@ function ensureSettingsForm(ctx) {
|
|
|
3703
3760
|
}
|
|
3704
3761
|
|
|
3705
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
|
+
|
|
3706
3769
|
const addLocale = (locale, dictionary) => {
|
|
3707
3770
|
try {
|
|
3708
3771
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -3755,50 +3818,31 @@ function apply(ctx) {
|
|
|
3755
3818
|
}
|
|
3756
3819
|
}
|
|
3757
3820
|
|
|
3758
|
-
// The
|
|
3759
|
-
//
|
|
3760
|
-
//
|
|
3761
|
-
//
|
|
3762
|
-
|
|
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', () =>
|
|
3763
3828
|
ctx.slots.register(
|
|
3764
3829
|
{
|
|
3765
|
-
name: '
|
|
3830
|
+
name: 'settings.section',
|
|
3766
3831
|
id: NS,
|
|
3767
|
-
order:
|
|
3768
|
-
label: () =>
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
// The core keys the seat as `<bundle name>#<row id>`; `bundle.name` may be the
|
|
3778
|
-
// package name or the short bundle name depending on the manager's view, so both
|
|
3779
|
-
// spellings are registered — the unused one is inert.
|
|
3780
|
-
// Row seat: key = ROW_CONFIG_KEY ('@goodandready/dsh-clinebot#dsh-clinebot').
|
|
3781
|
-
registerSlotWhenReady('plugins.row.config', () =>
|
|
3782
|
-
ctx.slots.register(
|
|
3783
|
-
{
|
|
3784
|
-
name: 'plugins.row.config',
|
|
3785
|
-
key: ROW_CONFIG_KEY,
|
|
3786
|
-
locale: NS,
|
|
3787
|
-
inject: () => ({ ctx }),
|
|
3788
|
-
},
|
|
3789
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3790
|
-
)
|
|
3791
|
-
)
|
|
3792
|
-
|
|
3793
|
-
registerSlotWhenReady('settings.plugin.item', () =>
|
|
3794
|
-
ctx.slots.register(
|
|
3795
|
-
{
|
|
3796
|
-
name: 'settings.plugin.item',
|
|
3797
|
-
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
|
+
},
|
|
3798
3842
|
locale: NS,
|
|
3799
3843
|
inject: () => ({ ctx }),
|
|
3800
3844
|
},
|
|
3801
|
-
(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 }))
|
|
3802
3846
|
)
|
|
3803
3847
|
)
|
|
3804
3848
|
|
|
@@ -3816,33 +3860,32 @@ function apply(ctx) {
|
|
|
3816
3860
|
)
|
|
3817
3861
|
)
|
|
3818
3862
|
|
|
3819
|
-
//
|
|
3820
|
-
|
|
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.
|
|
3875
|
+
|
|
3876
|
+
// Composer Session Cost Dock (conversation.composer.dock slot)
|
|
3877
|
+
registerSlotWhenReady('conversation.composer.dock', () =>
|
|
3821
3878
|
ctx.slots.register(
|
|
3822
3879
|
{
|
|
3823
|
-
name: '
|
|
3824
|
-
|
|
3880
|
+
name: 'conversation.composer.dock',
|
|
3881
|
+
id: 'clinebot-cost',
|
|
3882
|
+
order: 10,
|
|
3825
3883
|
locale: NS,
|
|
3826
3884
|
inject: () => ({ ctx }),
|
|
3827
3885
|
},
|
|
3828
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(
|
|
3886
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(ComposerCostDock, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3829
3887
|
)
|
|
3830
3888
|
)
|
|
3831
|
-
|
|
3832
|
-
// Settings Models Provider Card (settings.models.provider-card slot)
|
|
3833
|
-
for (const pKey of ['clinebot', 'llm-clinebot']) {
|
|
3834
|
-
registerSlotWhenReady('settings.models.provider-card', () =>
|
|
3835
|
-
ctx.slots.register(
|
|
3836
|
-
{
|
|
3837
|
-
name: 'settings.models.provider-card',
|
|
3838
|
-
key: pKey,
|
|
3839
|
-
locale: NS,
|
|
3840
|
-
inject: () => ({ ctx }),
|
|
3841
|
-
},
|
|
3842
|
-
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(ModelsProviderCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3843
|
-
)
|
|
3844
|
-
)
|
|
3845
|
-
}
|
|
3846
3889
|
}
|
|
3847
3890
|
|
|
3848
3891
|
module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
|