@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/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)?.status || 'loading'
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': '🔑 Authorization & API Key',
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': '🔑 Open API Keys ↗',
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': '👥 Multi-Account Failover Pool',
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': '📊 ClinePass Quota & Rate Limits',
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': '📅 Weekly Window',
103
+ 'quota.window_weekly': 'Weekly Window',
102
104
  'quota.used': '{pct}% used',
103
105
  'quota.reset_at': 'Reset: {time}',
104
- 'quota.window_monthly': '📅 Monthly Window',
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': '🧠 Reasoning',
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': '🎯 Model Picker in DSH ({enabled} of {total} active)',
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': '🔄 Check Plan Models',
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': '⚡ Original Specs',
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': '📈 Current DSH Session Telemetry',
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': '⚡ Diagnostics & Auto-Registration',
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': '🔑 授权与 API 密钥',
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': '🔑 打开 API 密钥页 ↗',
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': '👥 多账号故障转移与轮换池 (Failover)',
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': '📊 ClinePass 用量配额与限额监控',
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': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
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': '📈 当前 DSH 会话请求遥测',
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/chevron.js ---
487
- function FallbackChevron() {
488
- return React.createElement(
489
- 'svg',
490
- { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
491
- React.createElement('path', {
492
- d: 'M3.5 5.25L7 8.75L10.5 5.25',
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
- const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
503
- ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
483
+ primitives = require('@deepseek-ai/dsh-client-ui-primitives') || null
504
484
  } catch (_) {
505
- ChevronIcon = null
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:20px;padding:8px 0 32px;max-width:960px}
518
- .cb-chevron{display:inline-flex;align-items:center;justify-content:center;transition:transform .16s ease;color:var(--dsw-alias-label-tertiary)}
519
- .cb-chevron-open{transform:rotate(180deg)}
520
- .cb-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
521
- .cb-page-title{font-size:22px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
522
- .cb-page-sub{font-size:14px;color:var(--dsw-alias-label-secondary);line-height:1.5}
523
-
524
- .cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}
525
- .cb-section-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
526
- .cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
527
-
528
- .cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
529
- .cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:14px}
530
-
531
- .cb-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
532
- .cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}
533
- .cb-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent)}
534
- .cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}
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;gap:8px;align-items:center}
539
-
540
- .cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
541
- .cb-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
542
- .cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
543
- .cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88;visibility:visible !important}
544
- .cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}
545
- .cb-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent) !important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}
546
- .cb-btn-disabled{opacity:0.5;cursor:not-allowed}
547
-
548
- .cb-progress-card, .cb-bar-container{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
549
- .cb-progress-head, .cb-bar-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);width:100%}
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{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto}
552
- .cb-progress-track, .cb-bar-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;border:1px solid var(--dsw-alias-border-l2)}
553
- .cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-brand-primary, var(--dsw-alias-state-success-primary))}
554
- .cb-progress-bar-warn{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-warn-primary)}
555
- .cb-progress-bar-bad{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-error-primary)}
556
-
557
- .cb-table{width:100%;border-collapse:collapse;margin-top:8px}
558
- .cb-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
559
- .cb-table td{padding:10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
560
- .cb-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
561
-
562
- .cb-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px}
563
- .cb-alert-bad{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px}
564
- .cb-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px}
565
- .cb-banner-warning{padding:12px 16px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
566
- .cb-banner-exhausted{padding:12px 16px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:600}
567
- .cb-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
568
- .cb-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
569
- .cb-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
570
- .cb-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
571
-
572
- [class*="_footArea"] [class*="_footerActions"]{flex-direction:column}
573
- .cb-foot{box-sizing:border-box;flex:0 0 auto;width:100%;min-width:0;font:inherit;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:transparent;border:1px solid transparent;border-radius:10px;flex-direction:column;gap:6px;margin:0 0 4px;padding:8px;display:flex}
574
- .cb-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
575
- .cb-foot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
576
- .cb-footTop{align-items:center;gap:8px;min-width:0;display:flex}
577
- .cb-footName{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;font-size:13px;font-weight:500;line-height:20px}
578
- .cb-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
579
- .cb-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
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);border-radius:999px;height:5px;overflow:hidden}
583
- .cb-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
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;width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:transparent;border:1px solid transparent;border-radius:8px;flex:none;justify-content:center;align-items:center;margin:0 0 4px;padding:0;display:inline-flex}
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:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
589
- .cb-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
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
- .cb-mainOverlay{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
592
- .cb-mainInner{max-width:760px;margin:0 auto;padding:24px 20px 40px;flex-direction:column;gap:14px;display:flex;color:var(--dsw-alias-label-primary)}
593
- .cb-mainHeader{align-items:center;gap:10px;display:flex;flex-wrap:wrap;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:14px}
594
- .cb-mainTitle{margin:0;font-size:18px;font-weight:600;line-height:1.4}
595
- .cb-mainSub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
596
- .cb-mainCard{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:14px}
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: '10px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
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: '13px', fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } }, t('sidebar.show_quota')),
1541
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, t('sidebar.show_quota_hint'))
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('input', {
1544
- type: 'checkbox',
1545
- style: { cursor: 'pointer', width: '16px', height: '16px' },
1563
+ React.createElement(BooleanControl, {
1546
1564
  checked: showSidebarQuota !== false,
1547
- onChange: (e) => onToggleSidebarQuota && onToggleSidebarQuota(e.target.checked),
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: '8px' } },
1836
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' } },
1818
1837
  React.createElement(
1819
- 'label',
1820
- { style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '13px', fontWeight: '500' } },
1821
- React.createElement('input', {
1822
- type: 'checkbox',
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
- onChange: (e) => onConfigUpdate && onConfigUpdate({ filterModelsByPlan: e.target.checked }),
1826
- }),
1827
- React.createElement('span', null, t('models.filter_by_plan'))
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
- 'label',
2672
- { style: { display: 'inline-flex', alignItems: 'center', gap: '8px', cursor: 'pointer', whiteSpace: 'nowrap' } },
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: '13px',
2684
- fontWeight: 500,
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, open, t: propT }) {
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 [visible, setVisible] = React.useState(true)
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
- // Check configForms snapshot for showSidebarQuota toggle
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
- const handleOpen = () => {
2824
- if (typeof open === 'function') {
2825
- open()
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-railButton',
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-footHead' },
2869
- React.createElement('span', { className: 'cb-footLabel' }, t('dashboard.rolling_window')),
2870
- React.createElement('span', { className: 'cb-footAmount' }, `${100 - pct}% ${t('dashboard.remaining')}`)
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
- 'span',
2874
- { className: 'cb-footBar' },
2875
- React.createElement('span', {
2876
- className: isWarn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
2877
- style: { width: `${Math.min(100, Math.max(2, pct))}%` },
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-overlay.js ---
2885
- function DashboardOverlay(props) {
2886
- const ctx = props?.ctx
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
- return React.createElement(
2976
+ const body = React.createElement(
2932
2977
  'div',
2933
- { className: 'cb-mainOverlay', role: 'region', 'aria-label': t('dashboard.title') },
2978
+ { className: 'cb-dialog' },
2934
2979
  React.createElement(
2935
2980
  'div',
2936
- { className: 'cb-mainInner' },
2937
- // Header
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
- 'header',
2940
- { className: 'cb-mainHeader' },
2941
- React.createElement(
2942
- 'div',
2943
- { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
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
- err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
2991
+ err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
2980
2992
 
2981
- // Card 1: Account & Plan Status
2993
+ // Account and plan
2994
+ React.createElement(
2995
+ 'div',
2996
+ { className: 'cb-dialogCard' },
2982
2997
  React.createElement(
2983
2998
  'div',
2984
- { className: 'cb-mainCard' },
2999
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
2985
3000
  React.createElement(
2986
3001
  'div',
2987
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
2988
- React.createElement(
2989
- 'div',
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-mainCard' },
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
- { style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
3030
- t('quota.title')
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
- { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
3035
- data?.windows?.fiveHour
3036
- ? React.createElement(ProgressBar, {
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
- return React.createElement(
3132
- 'div',
3133
- { className: 'cb-providerCard', 'data-clinebot-provider-card': 'true' },
3134
- // Header
3026
+ // Quota windows
3135
3027
  React.createElement(
3136
3028
  'div',
3137
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
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', alignItems: 'center', gap: '8px' } },
3141
- React.createElement('span', { style: { fontSize: '18px' } }, '🤖'),
3142
- React.createElement('span', { style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('card.title'))
3143
- ),
3144
- React.createElement(
3145
- 'div',
3146
- { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
3147
- React.createElement(
3148
- 'span',
3149
- { className: keyConfigured ? 'cb-badge cb-badge-ok' : 'cb-badge cb-badge-warn' },
3150
- keyConfigured ? `✓ ${t('card.status_connected')}` : `⚠️ ${t('card.status_no_key')}`
3151
- ),
3152
- modelsCount > 0
3153
- ? React.createElement(
3154
- 'span',
3155
- { className: 'cb-badge cb-badge-ok' },
3156
- t('card.models_active', { count: modelsCount })
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
- // Quick Key Input Form
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
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
3080
+ { className: 'cb-dialogFallbackPanel', onClick: (e) => e.stopPropagation() },
3166
3081
  React.createElement(
3167
3082
  'div',
3168
- { className: 'cb-input-group' },
3169
- React.createElement('input', {
3170
- type: showKey ? 'text' : 'password',
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
- type: 'button',
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
- msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
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 snapshotStatus = useConfigFormsSnapshot(ctx)
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
- `🤖 ${t('header.title')}`,
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
- snapshotStatus === 'unavailable'
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 seat the Plugins page actually renders as the plugin's own page with the
3997
- // settings form: a card in the plugin list (`plugins.item`), exactly how
3998
- // @goodandready-private/dsh-agentrouter does it. `view: 'summary'` is the card's
3999
- // one-liner, `view: 'page'` is the body of that page.
4000
- registerSlotWhenReady('plugins.item', () =>
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: 'plugins.item',
3830
+ name: 'settings.section',
4004
3831
  id: NS,
4005
- order: 60,
4006
- label: () => 'clinebot',
4007
- locale: NS,
4008
- inject: () => ({ ctx }),
4009
- },
4010
- (props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
4011
- )
4012
- )
4013
-
4014
- // Row seat first (the seat the current core renders), legacy seat after it.
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(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
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
- // Central Overlay Dashboard (main slot)
4058
- registerSlotWhenReady('main', () =>
4059
- ctx.slots.register(
4060
- {
4061
- name: 'main',
4062
- key: 'clinebot-dashboard',
4063
- locale: NS,
4064
- inject: () => ({ ctx }),
4065
- },
4066
- (props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(DashboardOverlay, { ...props, ctx: (props && props.ctx) || ctx }))
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', () =>