@goodandready/dsh-clinebot 0.5.26 → 0.5.28
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 +18 -0
- package/lib/client.js +890 -4
- package/lib/config.js +36 -0
- package/lib/models.js +41 -33
- package/lib/proxy-token.js +2 -2
- package/lib/routes/models.js +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.28] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Sidebar Quota Meter (`sidebar.footer.action` slot) (Issue #229)**: Added live subscription quota indicator in DSH sidebar footer right above Settings. Supports collapsed rail mode with 20px dynamic circular ring (`MiniRing`) and wide mode with plan name (`ClinePass Pro`), rolling 5h quota progress bar, and percentage used.
|
|
12
|
+
- **Center-Column Dashboard Overlay (`main` slot) (Issue #229)**: Registered full overlay dashboard under key `clinebot-dashboard`, providing detailed view of 5-hour and weekly quota rolling windows, reset countdown timers, active account pool routing mode, and a close button (`×`) that restores the current chat view without page reload.
|
|
13
|
+
- **Native Models Provider Card (`settings.models.provider-card` slot) (Issue #230)**: Added native provider card registered under `clinebot` and `llm-clinebot` on DSH Settings -> Models page, featuring connection status badges, quick API key input and save into DSH credentials, active model count, and direct navigation to full ClineBot settings.
|
|
14
|
+
- **Sidebar Quota Toggle (`showSidebarQuota`) (Issue #229)**: Added volatile config field and settings toggle (default `true`) allowing users to easily enable or disable the sidebar quota meter.
|
|
15
|
+
|
|
16
|
+
## [0.5.27] - 2026-10-17
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Visible Models Multi-Select (`visibleModels`) (Issue #228)**: Users can now select an exact subset of models to expose in the DSH chat model picker. Includes search filtering, quick-select buttons (*All*, *Plan only*, *Clean stale*), and badge indicators for genuine context window and max output limits.
|
|
20
|
+
- **Subscription Plan Auto-Filtering (`filterModelsByPlan`) (Issue #228)**: Added toggle to automatically hide models that are not included in the active Cline account's subscription tier, preventing request failures due to tier mismatch.
|
|
21
|
+
- **Visible Models API Endpoint (Issue #228)**: Added `POST /dsh-clinebot/models/visible` for updating visible model selections with input validation.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Genuine Creator Limits Default (Issue #228)**: `createCuratedModel` now defaults context window and max tokens directly to genuine vendor factory limits (`origCtx` / `origMax`) instead of legacy hardcoded 128k/8k limits.
|
|
25
|
+
|
|
8
26
|
## [0.5.26] - 2026-10-17
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -123,7 +123,17 @@ const en = {
|
|
|
123
123
|
'ui.retry': 'Retry',
|
|
124
124
|
'update.check_failed': 'Failed to check for updates',
|
|
125
125
|
'update.retry': 'Retry check',
|
|
126
|
-
|
|
126
|
+
'models.filter_by_plan': 'Hide models not in active plan',
|
|
127
|
+
'models.filter_by_plan_hint': 'Automatically filter chat model picker to models included in your active ClinePass subscription plan.',
|
|
128
|
+
'models.visible_models': 'Visible Models in Chat',
|
|
129
|
+
'models.visible_models_hint': 'Choose which models appear in the DSH chat selector (empty means all supported models).',
|
|
130
|
+
'models.in_plan': 'In Plan',
|
|
131
|
+
'models.out_of_plan': 'Out of Plan',
|
|
132
|
+
'models.plan_only': 'Plan Only',
|
|
133
|
+
'models.show_all': 'Show All',
|
|
134
|
+
'models.clean_stale': 'Clean Stale ({count})',
|
|
135
|
+
'models.models_selected': '{selected} of {total} selected for chat',
|
|
136
|
+
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
127
137
|
'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.',
|
|
128
138
|
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
129
139
|
'models.sync': '🔄 Check Plan Models',
|
|
@@ -209,6 +219,28 @@ const en = {
|
|
|
209
219
|
'update.failed': 'Update failed: {error}',
|
|
210
220
|
'update.checking': 'Checking for updates…',
|
|
211
221
|
'update.up_to_date': 'Up to date',
|
|
222
|
+
'sidebar.quota_title': 'Cline Quota',
|
|
223
|
+
'sidebar.show_quota': 'Show quota meter in sidebar',
|
|
224
|
+
'sidebar.show_quota_hint': 'Display compact quota meter in DSH sidebar footer.',
|
|
225
|
+
'dashboard.title': 'Cline Quota & Status',
|
|
226
|
+
'dashboard.subtitle': 'Real-time subscription quota, rolling limits, and account pool status.',
|
|
227
|
+
'dashboard.close': 'Close dashboard',
|
|
228
|
+
'dashboard.refresh': 'Refresh',
|
|
229
|
+
'dashboard.refreshing': 'Refreshing…',
|
|
230
|
+
'dashboard.rolling_window': '5-Hour Window',
|
|
231
|
+
'dashboard.weekly_window': 'Weekly Window',
|
|
232
|
+
'dashboard.remaining': 'remaining',
|
|
233
|
+
'dashboard.used': 'used',
|
|
234
|
+
'dashboard.account_pool': 'Active Routing Account',
|
|
235
|
+
'dashboard.credits': 'Credit Balance',
|
|
236
|
+
'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',
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
const zh = {
|
|
@@ -296,7 +328,17 @@ const en = {
|
|
|
296
328
|
'ui.retry': '重试',
|
|
297
329
|
'update.check_failed': '检查更新失败',
|
|
298
330
|
'update.retry': '重试检查',
|
|
299
|
-
|
|
331
|
+
'models.filter_by_plan': '隐藏未订阅模型',
|
|
332
|
+
'models.filter_by_plan_hint': '在聊天模型选择器中自动隐藏当前 ClinePass 订阅套餐未包含的模型。',
|
|
333
|
+
'models.visible_models': '聊天可见模型',
|
|
334
|
+
'models.visible_models_hint': '选择在 DSH 聊天选择器中显示的模型(为空时显示全部可用模型)。',
|
|
335
|
+
'models.in_plan': '套餐内',
|
|
336
|
+
'models.out_of_plan': '套餐外',
|
|
337
|
+
'models.plan_only': '仅套餐模型',
|
|
338
|
+
'models.show_all': '显示全部',
|
|
339
|
+
'models.clean_stale': '清理过期 ({count})',
|
|
340
|
+
'models.models_selected': '已选择 {selected} / {total} 个模型用于聊天',
|
|
341
|
+
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
300
342
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
301
343
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
302
344
|
'models.sync': '🔄 同步订阅模型',
|
|
@@ -382,6 +424,28 @@ const en = {
|
|
|
382
424
|
'update.failed': '更新失败:{error}',
|
|
383
425
|
'update.checking': '正在检查更新…',
|
|
384
426
|
'update.up_to_date': '已是最新版本',
|
|
427
|
+
'sidebar.quota_title': 'Cline 额度',
|
|
428
|
+
'sidebar.show_quota': '在侧边栏显示额度条',
|
|
429
|
+
'sidebar.show_quota_hint': '在 DSH 侧边栏底部常驻显示额度 mini 进度指示器。',
|
|
430
|
+
'dashboard.title': 'Cline 额度与状态',
|
|
431
|
+
'dashboard.subtitle': '实时订阅额度、滑动窗口限制与账户池状态。',
|
|
432
|
+
'dashboard.close': '关闭面板',
|
|
433
|
+
'dashboard.refresh': '刷新',
|
|
434
|
+
'dashboard.refreshing': '正在刷新…',
|
|
435
|
+
'dashboard.rolling_window': '5小时滑动窗口',
|
|
436
|
+
'dashboard.weekly_window': '每周配额窗口',
|
|
437
|
+
'dashboard.remaining': '剩余',
|
|
438
|
+
'dashboard.used': '已用',
|
|
439
|
+
'dashboard.account_pool': '当前承载账户',
|
|
440
|
+
'dashboard.credits': '点数余额',
|
|
441
|
+
'dashboard.updated_at': '更新于',
|
|
442
|
+
'card.title': 'ClineBot 模型服务',
|
|
443
|
+
'card.status_connected': '已连接',
|
|
444
|
+
'card.status_no_key': '未配置密钥',
|
|
445
|
+
'card.open_settings': '完整设置与多账户',
|
|
446
|
+
'card.save_key': '保存密钥',
|
|
447
|
+
'card.key_saved': '密钥已保存',
|
|
448
|
+
'card.models_active': '{count} 个可用模型',
|
|
385
449
|
}
|
|
386
450
|
|
|
387
451
|
function makeT(dict, fallback) {
|
|
@@ -484,6 +548,34 @@ function ensureCss() {
|
|
|
484
548
|
.cb-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
|
|
485
549
|
.cb-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
486
550
|
.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)}
|
|
551
|
+
|
|
552
|
+
[class*="_footArea"] [class*="_footerActions"]{flex-direction:column}
|
|
553
|
+
.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}
|
|
554
|
+
.cb-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
|
|
555
|
+
.cb-foot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
556
|
+
.cb-footTop{align-items:center;gap:8px;min-width:0;display:flex}
|
|
557
|
+
.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}
|
|
558
|
+
.cb-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
|
|
559
|
+
.cb-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
|
|
560
|
+
.cb-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}
|
|
561
|
+
.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);border-radius:999px;height:5px;overflow:hidden}
|
|
563
|
+
.cb-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
|
|
564
|
+
.cb-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
565
|
+
.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;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}
|
|
567
|
+
.cb-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
568
|
+
.cb-railButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
569
|
+
.cb-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
|
|
570
|
+
|
|
571
|
+
.cb-mainOverlay{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
|
|
572
|
+
.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)}
|
|
573
|
+
.cb-mainHeader{align-items:center;gap:10px;display:flex;flex-wrap:wrap;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:14px}
|
|
574
|
+
.cb-mainTitle{margin:0;font-size:18px;font-weight:600;line-height:1.4}
|
|
575
|
+
.cb-mainSub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
|
|
576
|
+
.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}
|
|
577
|
+
|
|
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}
|
|
487
579
|
`
|
|
488
580
|
document.head.appendChild(style)
|
|
489
581
|
}
|
|
@@ -1269,7 +1361,7 @@ function AccountsSection({
|
|
|
1269
1361
|
}
|
|
1270
1362
|
|
|
1271
1363
|
// --- src/client/components/quota-section.js ---
|
|
1272
|
-
function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }) {
|
|
1364
|
+
function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, showSidebarQuota, onToggleSidebarQuota, t }) {
|
|
1273
1365
|
const isLowQuota = Boolean(
|
|
1274
1366
|
(usage?.windows?.weekly && typeof usage.windows.weekly.remainingPercent === 'number' && usage.windows.weekly.remainingPercent <= 10) ||
|
|
1275
1367
|
(usage?.windows?.monthly && typeof usage.windows.monthly.remainingPercent === 'number' && usage.windows.monthly.remainingPercent <= 10) ||
|
|
@@ -1359,6 +1451,22 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
1359
1451
|
t,
|
|
1360
1452
|
})
|
|
1361
1453
|
: null
|
|
1454
|
+
),
|
|
1455
|
+
React.createElement(
|
|
1456
|
+
'div',
|
|
1457
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '4px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1458
|
+
React.createElement(
|
|
1459
|
+
'div',
|
|
1460
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1461
|
+
React.createElement('span', { style: { fontSize: '13px', fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } }, t('sidebar.show_quota')),
|
|
1462
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, t('sidebar.show_quota_hint'))
|
|
1463
|
+
),
|
|
1464
|
+
React.createElement('input', {
|
|
1465
|
+
type: 'checkbox',
|
|
1466
|
+
style: { cursor: 'pointer', width: '16px', height: '16px' },
|
|
1467
|
+
checked: showSidebarQuota !== false,
|
|
1468
|
+
onChange: (e) => onToggleSidebarQuota && onToggleSidebarQuota(e.target.checked),
|
|
1469
|
+
})
|
|
1362
1470
|
)
|
|
1363
1471
|
)
|
|
1364
1472
|
: null
|
|
@@ -1537,6 +1645,254 @@ function CustomModelsSub({ customModels = [], onConfigUpdate, busy, t }) {
|
|
|
1537
1645
|
)
|
|
1538
1646
|
}
|
|
1539
1647
|
|
|
1648
|
+
// --- src/client/components/visible-models.js ---
|
|
1649
|
+
function formatModelLimits(num) {
|
|
1650
|
+
num = Number(num) || 0
|
|
1651
|
+
if (num >= 1000000) {
|
|
1652
|
+
const m = num / 1000000
|
|
1653
|
+
return `${m % 1 === 0 ? m : m.toFixed(1)}M`
|
|
1654
|
+
}
|
|
1655
|
+
if (num >= 1000) {
|
|
1656
|
+
const k = num / 1000
|
|
1657
|
+
return `${k % 1 === 0 ? k : k.toFixed(1)}K`
|
|
1658
|
+
}
|
|
1659
|
+
return String(num)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function VisibleModelsSub({
|
|
1663
|
+
modelsList = [],
|
|
1664
|
+
visibleModels = [],
|
|
1665
|
+
filterModelsByPlan = true,
|
|
1666
|
+
onConfigUpdate,
|
|
1667
|
+
busy,
|
|
1668
|
+
t,
|
|
1669
|
+
}) {
|
|
1670
|
+
const [open, setOpen] = React.useState(false)
|
|
1671
|
+
const [search, setSearch] = React.useState('')
|
|
1672
|
+
|
|
1673
|
+
const allModelIds = modelsList.map((m) => m.id)
|
|
1674
|
+
const allModelIdSet = new Set(allModelIds)
|
|
1675
|
+
const staleIds = Array.isArray(visibleModels) ? visibleModels.filter((id) => !allModelIdSet.has(id)) : []
|
|
1676
|
+
const isAllVisible = !Array.isArray(visibleModels) || visibleModels.length === 0
|
|
1677
|
+
const count = isAllVisible ? modelsList.length : visibleModels.length
|
|
1678
|
+
|
|
1679
|
+
function handleToggleModel(modelId) {
|
|
1680
|
+
if (typeof onConfigUpdate !== 'function') return
|
|
1681
|
+
if (isAllVisible) {
|
|
1682
|
+
const next = allModelIds.filter((id) => id !== modelId)
|
|
1683
|
+
onConfigUpdate({ visibleModels: next })
|
|
1684
|
+
} else {
|
|
1685
|
+
let next
|
|
1686
|
+
if (visibleModels.includes(modelId)) {
|
|
1687
|
+
next = visibleModels.filter((id) => id !== modelId)
|
|
1688
|
+
} else {
|
|
1689
|
+
next = [...visibleModels, modelId]
|
|
1690
|
+
}
|
|
1691
|
+
onConfigUpdate({ visibleModels: next })
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function handleSelectOnlyPlan() {
|
|
1696
|
+
if (typeof onConfigUpdate !== 'function') return
|
|
1697
|
+
const planOnly = modelsList.filter((m) => m.inPlan).map((m) => m.id)
|
|
1698
|
+
onConfigUpdate({ visibleModels: planOnly.length > 0 ? planOnly : allModelIds })
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function handleShowAll() {
|
|
1702
|
+
if (typeof onConfigUpdate !== 'function') return
|
|
1703
|
+
onConfigUpdate({ visibleModels: [] })
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function handleCleanStale() {
|
|
1707
|
+
if (typeof onConfigUpdate !== 'function') return
|
|
1708
|
+
const cleaned = visibleModels.filter((id) => allModelIdSet.has(id))
|
|
1709
|
+
onConfigUpdate({ visibleModels: cleaned })
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const filtered = modelsList.filter((m) => {
|
|
1713
|
+
if (!search.trim()) return true
|
|
1714
|
+
const q = search.trim().toLowerCase()
|
|
1715
|
+
return (
|
|
1716
|
+
(m.name && m.name.toLowerCase().includes(q)) ||
|
|
1717
|
+
(m.id && m.id.toLowerCase().includes(q)) ||
|
|
1718
|
+
(m.category && m.category.toLowerCase().includes(q))
|
|
1719
|
+
)
|
|
1720
|
+
})
|
|
1721
|
+
|
|
1722
|
+
return React.createElement(
|
|
1723
|
+
'div',
|
|
1724
|
+
{
|
|
1725
|
+
style: {
|
|
1726
|
+
display: 'flex',
|
|
1727
|
+
flexDirection: 'column',
|
|
1728
|
+
gap: '10px',
|
|
1729
|
+
padding: '12px 14px',
|
|
1730
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1731
|
+
borderRadius: '8px',
|
|
1732
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1733
|
+
marginBottom: '14px',
|
|
1734
|
+
},
|
|
1735
|
+
},
|
|
1736
|
+
React.createElement(
|
|
1737
|
+
'div',
|
|
1738
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
|
|
1739
|
+
React.createElement(
|
|
1740
|
+
'label',
|
|
1741
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '13px', fontWeight: '500' } },
|
|
1742
|
+
React.createElement('input', {
|
|
1743
|
+
type: 'checkbox',
|
|
1744
|
+
checked: !!filterModelsByPlan,
|
|
1745
|
+
disabled: !!busy,
|
|
1746
|
+
onChange: (e) => onConfigUpdate && onConfigUpdate({ filterModelsByPlan: e.target.checked }),
|
|
1747
|
+
}),
|
|
1748
|
+
React.createElement('span', null, t('models.filter_by_plan'))
|
|
1749
|
+
),
|
|
1750
|
+
React.createElement(
|
|
1751
|
+
'div',
|
|
1752
|
+
{ style: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' } },
|
|
1753
|
+
!isAllVisible
|
|
1754
|
+
? React.createElement(
|
|
1755
|
+
'button',
|
|
1756
|
+
{
|
|
1757
|
+
type: 'button',
|
|
1758
|
+
className: 'cb-btn',
|
|
1759
|
+
style: { padding: '4px 8px', fontSize: '12px' },
|
|
1760
|
+
disabled: !!busy,
|
|
1761
|
+
onClick: handleShowAll,
|
|
1762
|
+
},
|
|
1763
|
+
t('models.show_all')
|
|
1764
|
+
)
|
|
1765
|
+
: null,
|
|
1766
|
+
React.createElement(
|
|
1767
|
+
'button',
|
|
1768
|
+
{
|
|
1769
|
+
type: 'button',
|
|
1770
|
+
className: 'cb-btn',
|
|
1771
|
+
style: { padding: '4px 8px', fontSize: '12px' },
|
|
1772
|
+
disabled: !!busy,
|
|
1773
|
+
onClick: handleSelectOnlyPlan,
|
|
1774
|
+
},
|
|
1775
|
+
t('models.plan_only')
|
|
1776
|
+
),
|
|
1777
|
+
staleIds.length > 0
|
|
1778
|
+
? React.createElement(
|
|
1779
|
+
'button',
|
|
1780
|
+
{
|
|
1781
|
+
type: 'button',
|
|
1782
|
+
className: 'cb-btn',
|
|
1783
|
+
style: { padding: '4px 8px', fontSize: '12px', color: 'var(--dsw-alias-state-warn-primary)' },
|
|
1784
|
+
disabled: !!busy,
|
|
1785
|
+
onClick: handleCleanStale,
|
|
1786
|
+
},
|
|
1787
|
+
t('models.clean_stale', { count: staleIds.length })
|
|
1788
|
+
)
|
|
1789
|
+
: null,
|
|
1790
|
+
React.createElement(
|
|
1791
|
+
'button',
|
|
1792
|
+
{
|
|
1793
|
+
type: 'button',
|
|
1794
|
+
className: 'cb-btn',
|
|
1795
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
1796
|
+
onClick: () => setOpen(!open),
|
|
1797
|
+
},
|
|
1798
|
+
open ? '▲ ' + t('models.models_selected', { selected: count, total: modelsList.length }) : '▼ ' + t('models.models_selected', { selected: count, total: modelsList.length })
|
|
1799
|
+
)
|
|
1800
|
+
)
|
|
1801
|
+
),
|
|
1802
|
+
React.createElement(
|
|
1803
|
+
'div',
|
|
1804
|
+
{ style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', marginTop: '-4px' } },
|
|
1805
|
+
t('models.filter_by_plan_hint')
|
|
1806
|
+
),
|
|
1807
|
+
open
|
|
1808
|
+
? React.createElement(
|
|
1809
|
+
'div',
|
|
1810
|
+
{
|
|
1811
|
+
style: {
|
|
1812
|
+
display: 'flex',
|
|
1813
|
+
flexDirection: 'column',
|
|
1814
|
+
gap: '8px',
|
|
1815
|
+
paddingTop: '8px',
|
|
1816
|
+
borderTop: '1px solid var(--dsw-alias-border-l2)',
|
|
1817
|
+
},
|
|
1818
|
+
},
|
|
1819
|
+
React.createElement('input', {
|
|
1820
|
+
type: 'text',
|
|
1821
|
+
className: 'cb-input',
|
|
1822
|
+
style: { height: '30px', fontSize: '12px' },
|
|
1823
|
+
placeholder: t('models.search_placeholder'),
|
|
1824
|
+
value: search,
|
|
1825
|
+
onChange: (e) => setSearch(e.target.value),
|
|
1826
|
+
}),
|
|
1827
|
+
React.createElement(
|
|
1828
|
+
'div',
|
|
1829
|
+
{
|
|
1830
|
+
style: {
|
|
1831
|
+
display: 'flex',
|
|
1832
|
+
flexDirection: 'column',
|
|
1833
|
+
gap: '4px',
|
|
1834
|
+
maxHeight: '240px',
|
|
1835
|
+
overflowY: 'auto',
|
|
1836
|
+
paddingRight: '4px',
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
filtered.map((m) => {
|
|
1840
|
+
const isChecked = isAllVisible || visibleModels.includes(m.id)
|
|
1841
|
+
const ctxStr = `${formatModelLimits(m.contextLength)} / ${formatModelLimits(m.maxTokens)}`
|
|
1842
|
+
return React.createElement(
|
|
1843
|
+
'label',
|
|
1844
|
+
{
|
|
1845
|
+
key: m.id,
|
|
1846
|
+
style: {
|
|
1847
|
+
display: 'flex',
|
|
1848
|
+
alignItems: 'center',
|
|
1849
|
+
justifyContent: 'space-between',
|
|
1850
|
+
padding: '5px 8px',
|
|
1851
|
+
borderRadius: '6px',
|
|
1852
|
+
background: isChecked ? 'var(--dsw-alias-bg-layer-3)' : 'transparent',
|
|
1853
|
+
cursor: 'pointer',
|
|
1854
|
+
fontSize: '12px',
|
|
1855
|
+
opacity: m.inPlan ? 1 : 0.65,
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1858
|
+
React.createElement(
|
|
1859
|
+
'div',
|
|
1860
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px', flex: 1, minWidth: 0 } },
|
|
1861
|
+
React.createElement('input', {
|
|
1862
|
+
type: 'checkbox',
|
|
1863
|
+
checked: isChecked,
|
|
1864
|
+
onChange: () => handleToggleModel(m.id),
|
|
1865
|
+
}),
|
|
1866
|
+
React.createElement('span', { style: { fontWeight: '500', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, m.name),
|
|
1867
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px' } }, `(${m.id})`)
|
|
1868
|
+
),
|
|
1869
|
+
React.createElement(
|
|
1870
|
+
'div',
|
|
1871
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
1872
|
+
React.createElement(
|
|
1873
|
+
'span',
|
|
1874
|
+
{
|
|
1875
|
+
className: 'cb-badge',
|
|
1876
|
+
style: { fontSize: '11px', padding: '2px 6px', fontWeight: '600' },
|
|
1877
|
+
title: 'Context Window / Max Output Tokens',
|
|
1878
|
+
},
|
|
1879
|
+
ctxStr
|
|
1880
|
+
),
|
|
1881
|
+
m.inPlan
|
|
1882
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 6px' } }, t('models.in_plan'))
|
|
1883
|
+
: React.createElement('span', { className: 'cb-badge', style: { fontSize: '10px', padding: '1px 6px', color: 'var(--dsw-alias-label-secondary)' } }, t('models.out_of_plan')),
|
|
1884
|
+
!m.isContextOverridden
|
|
1885
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { fontSize: '10px', padding: '1px 5px' } }, '原厂')
|
|
1886
|
+
: null
|
|
1887
|
+
)
|
|
1888
|
+
)
|
|
1889
|
+
})
|
|
1890
|
+
)
|
|
1891
|
+
)
|
|
1892
|
+
: null
|
|
1893
|
+
)
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1540
1896
|
// --- src/client/components/models-section.js ---
|
|
1541
1897
|
function ModelsSection({
|
|
1542
1898
|
modelsList,
|
|
@@ -1553,6 +1909,8 @@ function ModelsSection({
|
|
|
1553
1909
|
defaultModelWarning,
|
|
1554
1910
|
customModels,
|
|
1555
1911
|
modelReasoningDefaults,
|
|
1912
|
+
visibleModels,
|
|
1913
|
+
filterModelsByPlan,
|
|
1556
1914
|
onConfigUpdate,
|
|
1557
1915
|
onUpdateReasoningEffort,
|
|
1558
1916
|
t,
|
|
@@ -1604,6 +1962,14 @@ function ModelsSection({
|
|
|
1604
1962
|
defaultModelWarning
|
|
1605
1963
|
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, `⚠️ ${defaultModelWarning}`)
|
|
1606
1964
|
: null,
|
|
1965
|
+
React.createElement(VisibleModelsSub, {
|
|
1966
|
+
modelsList,
|
|
1967
|
+
visibleModels,
|
|
1968
|
+
filterModelsByPlan,
|
|
1969
|
+
onConfigUpdate,
|
|
1970
|
+
busy,
|
|
1971
|
+
t,
|
|
1972
|
+
}),
|
|
1607
1973
|
React.createElement(
|
|
1608
1974
|
'div',
|
|
1609
1975
|
{ className: 'cb-section-title' },
|
|
@@ -2295,6 +2661,482 @@ function DiagSection({
|
|
|
2295
2661
|
)
|
|
2296
2662
|
}
|
|
2297
2663
|
|
|
2664
|
+
// --- src/client/components/sidebar-quota.js ---
|
|
2665
|
+
function MiniRing({ percent, size = 18, warn = false }) {
|
|
2666
|
+
const r = 7
|
|
2667
|
+
const c = 2 * Math.PI * r
|
|
2668
|
+
const pct = Math.max(0, Math.min(100, Number(percent) || 0))
|
|
2669
|
+
const offset = c - (pct / 100) * c
|
|
2670
|
+
const strokeColor = warn ? 'var(--dsw-alias-state-error-primary)' : 'var(--dsw-alias-brand-primary)'
|
|
2671
|
+
|
|
2672
|
+
return React.createElement(
|
|
2673
|
+
'svg',
|
|
2674
|
+
{
|
|
2675
|
+
className: 'cb-glyph',
|
|
2676
|
+
width: size,
|
|
2677
|
+
height: size,
|
|
2678
|
+
viewBox: '0 0 20 20',
|
|
2679
|
+
fill: 'none',
|
|
2680
|
+
},
|
|
2681
|
+
React.createElement('circle', {
|
|
2682
|
+
cx: 10,
|
|
2683
|
+
cy: 10,
|
|
2684
|
+
r,
|
|
2685
|
+
stroke: 'var(--dsw-alias-bg-layer-2)',
|
|
2686
|
+
strokeWidth: 2.5,
|
|
2687
|
+
}),
|
|
2688
|
+
React.createElement('circle', {
|
|
2689
|
+
cx: 10,
|
|
2690
|
+
cy: 10,
|
|
2691
|
+
r,
|
|
2692
|
+
stroke: strokeColor,
|
|
2693
|
+
strokeWidth: 2.5,
|
|
2694
|
+
strokeDasharray: c,
|
|
2695
|
+
strokeDashoffset: offset,
|
|
2696
|
+
strokeLinecap: 'round',
|
|
2697
|
+
transform: 'rotate(-90 10 10)',
|
|
2698
|
+
style: { transition: 'stroke-dashoffset 0.3s ease' },
|
|
2699
|
+
})
|
|
2700
|
+
)
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
function SidebarQuota({ ctx, wide, open, t: propT }) {
|
|
2704
|
+
const t = propT || makeT(en, en)
|
|
2705
|
+
const [data, setData] = React.useState(null)
|
|
2706
|
+
const [visible, setVisible] = React.useState(true)
|
|
2707
|
+
|
|
2708
|
+
const loadQuota = React.useCallback(async () => {
|
|
2709
|
+
try {
|
|
2710
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/usage`, { cache: 'no-store' })
|
|
2711
|
+
if (!res.ok) return
|
|
2712
|
+
const json = await res.json().catch(() => null)
|
|
2713
|
+
if (json && json.ok) setData(json)
|
|
2714
|
+
} catch {
|
|
2715
|
+
// quiet on network fail
|
|
2716
|
+
}
|
|
2717
|
+
}, [])
|
|
2718
|
+
|
|
2719
|
+
React.useEffect(() => {
|
|
2720
|
+
let timer = null
|
|
2721
|
+
const scheduleNext = () => {
|
|
2722
|
+
timer = setTimeout(async () => {
|
|
2723
|
+
await loadQuota()
|
|
2724
|
+
scheduleNext()
|
|
2725
|
+
}, 60000)
|
|
2726
|
+
}
|
|
2727
|
+
loadQuota()
|
|
2728
|
+
scheduleNext()
|
|
2729
|
+
return () => { if (timer) clearTimeout(timer) }
|
|
2730
|
+
}, [loadQuota])
|
|
2731
|
+
|
|
2732
|
+
// Check configForms snapshot for showSidebarQuota toggle
|
|
2733
|
+
const config = useConfigFormsSnapshot(ctx)
|
|
2734
|
+
const show = config?.showSidebarQuota !== false
|
|
2735
|
+
if (!show || !visible) return null
|
|
2736
|
+
|
|
2737
|
+
const fiveHour = data?.windows?.fiveHour
|
|
2738
|
+
const weekly = data?.windows?.weekly
|
|
2739
|
+
const activeWindow = fiveHour || weekly
|
|
2740
|
+
const pct = Math.round(activeWindow?.percentUsed || 0)
|
|
2741
|
+
const isWarn = pct >= 85
|
|
2742
|
+
const planName = data?.plan || t('title')
|
|
2743
|
+
|
|
2744
|
+
const handleOpen = () => {
|
|
2745
|
+
if (typeof open === 'function') {
|
|
2746
|
+
open()
|
|
2747
|
+
} else {
|
|
2748
|
+
ctx?.get?.('layout')?.selectPanel?.('clinebot-dashboard')
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
const tooltip = `${t('sidebar.quota_title')}: ${pct}% ${t('dashboard.used')} (${planName})`
|
|
2753
|
+
|
|
2754
|
+
if (!wide) {
|
|
2755
|
+
return React.createElement(
|
|
2756
|
+
'button',
|
|
2757
|
+
{
|
|
2758
|
+
type: 'button',
|
|
2759
|
+
className: 'cb-railButton',
|
|
2760
|
+
'aria-label': tooltip,
|
|
2761
|
+
title: tooltip,
|
|
2762
|
+
onClick: handleOpen,
|
|
2763
|
+
},
|
|
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
|
+
React.createElement(
|
|
2788
|
+
'div',
|
|
2789
|
+
{ className: 'cb-footHead' },
|
|
2790
|
+
React.createElement('span', { className: 'cb-footLabel' }, t('dashboard.rolling_window')),
|
|
2791
|
+
React.createElement('span', { className: 'cb-footAmount' }, `${100 - pct}% ${t('dashboard.remaining')}`)
|
|
2792
|
+
),
|
|
2793
|
+
React.createElement(
|
|
2794
|
+
'span',
|
|
2795
|
+
{ className: 'cb-footBar' },
|
|
2796
|
+
React.createElement('span', {
|
|
2797
|
+
className: isWarn ? 'cb-footFill cb-footFillWarn' : 'cb-footFill',
|
|
2798
|
+
style: { width: `${Math.min(100, Math.max(2, pct))}%` },
|
|
2799
|
+
})
|
|
2800
|
+
)
|
|
2801
|
+
)
|
|
2802
|
+
)
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// --- src/client/components/dashboard-overlay.js ---
|
|
2806
|
+
function DashboardOverlay(props) {
|
|
2807
|
+
const ctx = props?.ctx
|
|
2808
|
+
const t = props?.t || makeT(en, en)
|
|
2809
|
+
const [data, setData] = React.useState(null)
|
|
2810
|
+
const [status, setStatus] = React.useState(null)
|
|
2811
|
+
const [loading, setLoading] = React.useState(false)
|
|
2812
|
+
const [updatedAt, setUpdatedAt] = React.useState('')
|
|
2813
|
+
const [err, setErr] = React.useState('')
|
|
2814
|
+
|
|
2815
|
+
const handleClose = () => {
|
|
2816
|
+
if (typeof props?.close === 'function') {
|
|
2817
|
+
props.close()
|
|
2818
|
+
} else {
|
|
2819
|
+
ctx?.get?.('layout')?.selectPanel?.(null)
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
const loadData = React.useCallback(async () => {
|
|
2824
|
+
setLoading(true)
|
|
2825
|
+
setErr('')
|
|
2826
|
+
try {
|
|
2827
|
+
const [uRes, sRes] = await Promise.all([
|
|
2828
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/usage`, { cache: 'no-store' }),
|
|
2829
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/status`, { cache: 'no-store' }),
|
|
2830
|
+
])
|
|
2831
|
+
const uJson = await uRes.json().catch(() => ({}))
|
|
2832
|
+
const sJson = await sRes.json().catch(() => ({}))
|
|
2833
|
+
if (uJson?.ok) setData(uJson)
|
|
2834
|
+
if (sJson?.ok) setStatus(sJson)
|
|
2835
|
+
setUpdatedAt(new Date().toLocaleTimeString())
|
|
2836
|
+
} catch (e) {
|
|
2837
|
+
setErr(String(e?.message || e))
|
|
2838
|
+
} finally {
|
|
2839
|
+
setLoading(false)
|
|
2840
|
+
}
|
|
2841
|
+
}, [])
|
|
2842
|
+
|
|
2843
|
+
React.useEffect(() => {
|
|
2844
|
+
loadData()
|
|
2845
|
+
}, [loadData])
|
|
2846
|
+
|
|
2847
|
+
const plan = data?.plan || status?.config?.defaultModel || 'ClinePass'
|
|
2848
|
+
const email = data?.user?.email || ''
|
|
2849
|
+
const activeAcc = status?.config?.activeAccount || 'Auto (Least-used)'
|
|
2850
|
+
const enabledCount = status?.config?.enabledModels?.length || 0
|
|
2851
|
+
|
|
2852
|
+
return React.createElement(
|
|
2853
|
+
'div',
|
|
2854
|
+
{ className: 'cb-mainOverlay', role: 'region', 'aria-label': t('dashboard.title') },
|
|
2855
|
+
React.createElement(
|
|
2856
|
+
'div',
|
|
2857
|
+
{ className: 'cb-mainInner' },
|
|
2858
|
+
// Header
|
|
2859
|
+
React.createElement(
|
|
2860
|
+
'header',
|
|
2861
|
+
{ className: 'cb-mainHeader' },
|
|
2862
|
+
React.createElement(
|
|
2863
|
+
'div',
|
|
2864
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
|
|
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
|
+
),
|
|
2899
|
+
|
|
2900
|
+
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
|
|
2901
|
+
|
|
2902
|
+
// Card 1: Account & Plan Status
|
|
2903
|
+
React.createElement(
|
|
2904
|
+
'div',
|
|
2905
|
+
{ className: 'cb-mainCard' },
|
|
2906
|
+
React.createElement(
|
|
2907
|
+
'div',
|
|
2908
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2909
|
+
React.createElement(
|
|
2910
|
+
'div',
|
|
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
|
+
)
|
|
2925
|
+
),
|
|
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
|
+
)
|
|
2942
|
+
),
|
|
2943
|
+
|
|
2944
|
+
// Card 2: Quota Windows
|
|
2945
|
+
React.createElement(
|
|
2946
|
+
'div',
|
|
2947
|
+
{ className: 'cb-mainCard' },
|
|
2948
|
+
React.createElement(
|
|
2949
|
+
'div',
|
|
2950
|
+
{ style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
|
|
2951
|
+
t('quota.title')
|
|
2952
|
+
),
|
|
2953
|
+
React.createElement(
|
|
2954
|
+
'div',
|
|
2955
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
2956
|
+
data?.windows?.fiveHour
|
|
2957
|
+
? React.createElement(ProgressBar, {
|
|
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
|
|
2981
|
+
)
|
|
2982
|
+
)
|
|
2983
|
+
)
|
|
2984
|
+
)
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// --- src/client/components/models-provider-card.js ---
|
|
2988
|
+
function ModelsProviderCard(props) {
|
|
2989
|
+
const ctx = props?.ctx
|
|
2990
|
+
const t = props?.t || makeT(en, en)
|
|
2991
|
+
const [status, setStatus] = React.useState(null)
|
|
2992
|
+
const [apiKey, setApiKey] = React.useState('')
|
|
2993
|
+
const [showKey, setShowKey] = React.useState(false)
|
|
2994
|
+
const [saving, setSaving] = React.useState(false)
|
|
2995
|
+
const [msg, setMsg] = React.useState('')
|
|
2996
|
+
const [err, setErr] = React.useState('')
|
|
2997
|
+
|
|
2998
|
+
const loadStatus = React.useCallback(async () => {
|
|
2999
|
+
try {
|
|
3000
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
|
|
3001
|
+
if (!res.ok) return
|
|
3002
|
+
const json = await res.json().catch(() => null)
|
|
3003
|
+
if (json && json.ok) setStatus(json)
|
|
3004
|
+
} catch {
|
|
3005
|
+
// quiet on network fail
|
|
3006
|
+
}
|
|
3007
|
+
}, [])
|
|
3008
|
+
|
|
3009
|
+
React.useEffect(() => {
|
|
3010
|
+
loadStatus()
|
|
3011
|
+
}, [loadStatus])
|
|
3012
|
+
|
|
3013
|
+
const keyConfigured = Boolean(status?.keyPresent || status?.config?.hasKey)
|
|
3014
|
+
const modelsCount = status?.config?.enabledModels?.length || status?.availableModels?.length || 0
|
|
3015
|
+
|
|
3016
|
+
const handleSaveKey = async () => {
|
|
3017
|
+
const keyVal = String(apiKey || '').trim()
|
|
3018
|
+
if (!keyVal) {
|
|
3019
|
+
setErr(t('key.empty_err'))
|
|
3020
|
+
return
|
|
3021
|
+
}
|
|
3022
|
+
setSaving(true)
|
|
3023
|
+
setErr('')
|
|
3024
|
+
setMsg('')
|
|
3025
|
+
try {
|
|
3026
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/save-key`, {
|
|
3027
|
+
method: 'POST',
|
|
3028
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3029
|
+
body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: status?.config?.apiKeyEnv }),
|
|
3030
|
+
})
|
|
3031
|
+
const data = await res.json().catch(() => ({}))
|
|
3032
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
3033
|
+
setApiKey('')
|
|
3034
|
+
setMsg(t('card.key_saved'))
|
|
3035
|
+
await loadStatus()
|
|
3036
|
+
} catch (e) {
|
|
3037
|
+
setErr(String(e?.message || e))
|
|
3038
|
+
} finally {
|
|
3039
|
+
setSaving(false)
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
const handleOpenSettings = () => {
|
|
3044
|
+
const layout = ctx?.get?.('layout')
|
|
3045
|
+
if (typeof layout?.selectPanel === 'function') {
|
|
3046
|
+
layout.selectPanel('clinebot-dashboard')
|
|
3047
|
+
} else if (typeof props?.openSettings === 'function') {
|
|
3048
|
+
props.openSettings()
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
return React.createElement(
|
|
3053
|
+
'div',
|
|
3054
|
+
{ className: 'cb-providerCard', 'data-clinebot-provider-card': 'true' },
|
|
3055
|
+
// Header
|
|
3056
|
+
React.createElement(
|
|
3057
|
+
'div',
|
|
3058
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3059
|
+
React.createElement(
|
|
3060
|
+
'div',
|
|
3061
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
3062
|
+
React.createElement('span', { style: { fontSize: '18px' } }, '🤖'),
|
|
3063
|
+
React.createElement('span', { style: { fontSize: '15px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('card.title'))
|
|
3064
|
+
),
|
|
3065
|
+
React.createElement(
|
|
3066
|
+
'div',
|
|
3067
|
+
{ style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
3068
|
+
React.createElement(
|
|
3069
|
+
'span',
|
|
3070
|
+
{ className: keyConfigured ? 'cb-badge cb-badge-ok' : 'cb-badge cb-badge-warn' },
|
|
3071
|
+
keyConfigured ? `✓ ${t('card.status_connected')}` : `⚠️ ${t('card.status_no_key')}`
|
|
3072
|
+
),
|
|
3073
|
+
modelsCount > 0
|
|
3074
|
+
? React.createElement(
|
|
3075
|
+
'span',
|
|
3076
|
+
{ className: 'cb-badge cb-badge-ok' },
|
|
3077
|
+
t('card.models_active', { count: modelsCount })
|
|
3078
|
+
)
|
|
3079
|
+
: null
|
|
3080
|
+
)
|
|
3081
|
+
),
|
|
3082
|
+
|
|
3083
|
+
// Quick Key Input Form
|
|
3084
|
+
React.createElement(
|
|
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',
|
|
3110
|
+
{
|
|
3111
|
+
type: 'button',
|
|
3112
|
+
className: 'cb-btn cb-btn-primary',
|
|
3113
|
+
disabled: saving || !apiKey.trim(),
|
|
3114
|
+
onClick: handleSaveKey,
|
|
3115
|
+
},
|
|
3116
|
+
saving ? t('key.saving') : t('card.save_key')
|
|
3117
|
+
)
|
|
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
|
+
)
|
|
3137
|
+
)
|
|
3138
|
+
}
|
|
3139
|
+
|
|
2298
3140
|
// --- src/client/settings-page.js ---
|
|
2299
3141
|
function SettingsPage(props) {
|
|
2300
3142
|
const ctx = props?.ctx
|
|
@@ -2742,7 +3584,7 @@ function SettingsPage(props) {
|
|
|
2742
3584
|
}),
|
|
2743
3585
|
|
|
2744
3586
|
// Quota Warning & Dashboard Card
|
|
2745
|
-
React.createElement(QuotaSection, { keyPresent, status, usage, busy, handleRefreshQuota, t }),
|
|
3587
|
+
React.createElement(QuotaSection, { keyPresent, status, usage, busy, handleRefreshQuota, showSidebarQuota: draft?.showSidebarQuota !== false, onToggleSidebarQuota: (v) => handleConfigPatch({ showSidebarQuota: v }), t }),
|
|
2746
3588
|
|
|
2747
3589
|
// Card 3: Model Picker Management
|
|
2748
3590
|
React.createElement(ModelsSection, {
|
|
@@ -2760,6 +3602,8 @@ function SettingsPage(props) {
|
|
|
2760
3602
|
defaultModelWarning: status?.config?.defaultModelWarning || '',
|
|
2761
3603
|
customModels: draft?.customModels || [],
|
|
2762
3604
|
modelReasoningDefaults: draft?.modelReasoningDefaults || {},
|
|
3605
|
+
visibleModels: draft?.visibleModels || [],
|
|
3606
|
+
filterModelsByPlan: draft?.filterModelsByPlan !== false,
|
|
2763
3607
|
onConfigUpdate: handleConfigPatch,
|
|
2764
3608
|
onUpdateReasoningEffort: (modelId, effort) => {
|
|
2765
3609
|
const next = { ...(draft?.modelReasoningDefaults || {}) }
|
|
@@ -2957,6 +3801,48 @@ function apply(ctx) {
|
|
|
2957
3801
|
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2958
3802
|
)
|
|
2959
3803
|
)
|
|
3804
|
+
|
|
3805
|
+
// Sidebar Quota Meter (sidebar.footer.action slot)
|
|
3806
|
+
registerSlotWhenReady('sidebar.footer.action', () =>
|
|
3807
|
+
ctx.slots.register(
|
|
3808
|
+
{
|
|
3809
|
+
name: 'sidebar.footer.action',
|
|
3810
|
+
id: 'clinebot-quota',
|
|
3811
|
+
order: 20,
|
|
3812
|
+
locale: NS,
|
|
3813
|
+
inject: () => ({ ctx }),
|
|
3814
|
+
},
|
|
3815
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(SidebarQuota, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3816
|
+
)
|
|
3817
|
+
)
|
|
3818
|
+
|
|
3819
|
+
// Central Overlay Dashboard (main slot)
|
|
3820
|
+
registerSlotWhenReady('main', () =>
|
|
3821
|
+
ctx.slots.register(
|
|
3822
|
+
{
|
|
3823
|
+
name: 'main',
|
|
3824
|
+
key: 'clinebot-dashboard',
|
|
3825
|
+
locale: NS,
|
|
3826
|
+
inject: () => ({ ctx }),
|
|
3827
|
+
},
|
|
3828
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(DashboardOverlay, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
3829
|
+
)
|
|
3830
|
+
)
|
|
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
|
+
}
|
|
2960
3846
|
}
|
|
2961
3847
|
|
|
2962
3848
|
module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
|
package/lib/config.js
CHANGED
|
@@ -37,6 +37,12 @@ export const Config = z.object({
|
|
|
37
37
|
.description('Default model ID for chat and smoke tests.').volatile(),
|
|
38
38
|
disabledModels: z.array(z.string()).default([])
|
|
39
39
|
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
|
|
40
|
+
filterModelsByPlan: z.boolean().default(true)
|
|
41
|
+
.description('Automatically hide models not included in active subscription plan from chat picker.').volatile(),
|
|
42
|
+
visibleModels: z.array(z.string()).default([])
|
|
43
|
+
.description('User-selected visible models in chat model picker (empty means all supported models).').volatile(),
|
|
44
|
+
showSidebarQuota: z.boolean().default(true)
|
|
45
|
+
.description('Display compact quota meter in DSH sidebar footer.').volatile(),
|
|
40
46
|
enabledModels: z.array(z.string()).default([])
|
|
41
47
|
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
42
48
|
dynamicModels: z.array(z.object({
|
|
@@ -144,7 +150,34 @@ export function publicConfig(cfg) {
|
|
|
144
150
|
disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
const filterModelsByPlan = cfg?.filterModelsByPlan !== false
|
|
154
|
+
const visibleModels = Array.isArray(cfg?.visibleModels) ? cfg.visibleModels.map(migrateModelId) : []
|
|
155
|
+
|
|
156
|
+
const planModelSet = new Set(dynamic.map((m) => m.id))
|
|
157
|
+
for (const m of allModels) {
|
|
158
|
+
if (m.inPlan === undefined) {
|
|
159
|
+
m.inPlan = isSynced ? (planModelSet.has(m.id) || planModelSet.has(migrateModelId(m.id)) || Boolean(m.isCustom)) : true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
147
163
|
let activeIds = getActiveModelIds(allDefaultIds, disabledList)
|
|
164
|
+
if (filterModelsByPlan && isSynced && planModelSet.size > 0) {
|
|
165
|
+
const inPlanIds = activeIds.filter((id) => {
|
|
166
|
+
const m = allModels.find((mod) => mod.id === id)
|
|
167
|
+
return m ? m.inPlan : true
|
|
168
|
+
})
|
|
169
|
+
if (inPlanIds.length > 0) {
|
|
170
|
+
activeIds = inPlanIds
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (visibleModels.length > 0) {
|
|
174
|
+
const visibleSet = new Set(visibleModels)
|
|
175
|
+
const filteredByVisible = activeIds.filter((id) => visibleSet.has(id))
|
|
176
|
+
if (filteredByVisible.length > 0) {
|
|
177
|
+
activeIds = filteredByVisible
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
if (!activeIds.length && allDefaultIds.length) {
|
|
149
182
|
activeIds = [allDefaultIds[0]]
|
|
150
183
|
}
|
|
@@ -176,6 +209,9 @@ export function publicConfig(cfg) {
|
|
|
176
209
|
proxyMode: cfg?.proxyMode !== false,
|
|
177
210
|
statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
|
|
178
211
|
disabledModels: disabledList,
|
|
212
|
+
filterModelsByPlan,
|
|
213
|
+
visibleModels,
|
|
214
|
+
showSidebarQuota: cfg?.showSidebarQuota !== false,
|
|
179
215
|
enabledModels: activeIds,
|
|
180
216
|
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
|
181
217
|
smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
|
package/lib/models.js
CHANGED
|
@@ -85,7 +85,7 @@ export function getOriginalModelProvider(nameOrId) {
|
|
|
85
85
|
|
|
86
86
|
function createCuratedModel({
|
|
87
87
|
id, name, description,
|
|
88
|
-
ctx
|
|
88
|
+
ctx, maxTokens,
|
|
89
89
|
origCtx, origMax, origProv,
|
|
90
90
|
input = ['text'], category = 'general',
|
|
91
91
|
recommended = false, reasoningEfforts,
|
|
@@ -93,11 +93,13 @@ function createCuratedModel({
|
|
|
93
93
|
const c = origCtx || getOriginalModelContext(id)
|
|
94
94
|
const p = origProv || getOriginalModelProvider(id)
|
|
95
95
|
const om = origMax || getOriginalModelMaxTokens(id)
|
|
96
|
+
const activeCtx = ctx || c
|
|
97
|
+
const activeMax = maxTokens || om
|
|
96
98
|
return {
|
|
97
99
|
id, name, description,
|
|
98
|
-
clineContextLength:
|
|
100
|
+
clineContextLength: activeCtx, defaultContextLength: activeCtx, contextLength: activeCtx,
|
|
99
101
|
originalContextLength: c, originalProvider: p, originalMaxTokens: om,
|
|
100
|
-
maxTokens, defaultMaxTokens:
|
|
102
|
+
maxTokens: activeMax, defaultMaxTokens: activeMax,
|
|
101
103
|
input, category, recommended, isCustom: false,
|
|
102
104
|
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
103
105
|
}
|
|
@@ -107,76 +109,65 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
107
109
|
createCuratedModel({
|
|
108
110
|
id: 'cline-pass/deepseek-v4.1-flash',
|
|
109
111
|
name: 'DeepSeek V4.1 Flash',
|
|
110
|
-
description: 'High-speed reasoning & code completion model optimized for agentic loops.',
|
|
111
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
|
|
112
|
+
description: 'High-speed reasoning & code completion model optimized for agentic loops.', input: ['text', 'image'], category: 'coding', recommended: true,
|
|
112
113
|
reasoningEfforts: ['low', 'medium', 'high'],
|
|
113
114
|
}),
|
|
114
115
|
createCuratedModel({
|
|
115
116
|
id: 'cline-pass/deepseek-v4-pro',
|
|
116
117
|
name: 'DeepSeek V4 Pro',
|
|
117
|
-
description: 'Flagship reasoning and multi-turn architectural coding model.',
|
|
118
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: true,
|
|
118
|
+
description: 'Flagship reasoning and multi-turn architectural coding model.', input: ['text', 'image'], category: 'coding', recommended: true,
|
|
119
119
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
120
120
|
}),
|
|
121
121
|
createCuratedModel({
|
|
122
122
|
id: 'cline-pass/glm-5.2',
|
|
123
123
|
name: 'GLM 5.2',
|
|
124
|
-
description: 'Bilingual general & coding model with strong instruction following.',
|
|
125
|
-
ctx: 128000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
|
|
124
|
+
description: 'Bilingual general & coding model with strong instruction following.', input: ['text'], category: 'general', recommended: false,
|
|
126
125
|
reasoningEfforts: ['low', 'medium', 'high'],
|
|
127
126
|
}),
|
|
128
127
|
createCuratedModel({
|
|
129
128
|
id: 'cline-pass/kimi-k3',
|
|
130
129
|
name: 'Kimi K3',
|
|
131
|
-
description: 'Long-context reasoning & document synthesis model by Moonshot AI.',
|
|
132
|
-
ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: true,
|
|
130
|
+
description: 'Long-context reasoning & document synthesis model by Moonshot AI.', input: ['text'], category: 'reasoning', recommended: true,
|
|
133
131
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
134
132
|
}),
|
|
135
133
|
createCuratedModel({
|
|
136
134
|
id: 'cline-pass/kimi-k2.7-code',
|
|
137
135
|
name: 'Kimi K2.7 Code',
|
|
138
|
-
description: 'Specialized coding model with agentic tool calling support.',
|
|
139
|
-
ctx: 200000, maxTokens: 8192, input: ['text'], category: 'coding', recommended: false,
|
|
136
|
+
description: 'Specialized coding model with agentic tool calling support.', input: ['text'], category: 'coding', recommended: false,
|
|
140
137
|
}),
|
|
141
138
|
createCuratedModel({
|
|
142
139
|
id: 'cline-pass/kimi-k2.6',
|
|
143
140
|
name: 'Kimi K2.6',
|
|
144
|
-
description: 'Balanced long-context conversational model.',
|
|
145
|
-
ctx: 200000, maxTokens: 8192, input: ['text'], category: 'general', recommended: false,
|
|
141
|
+
description: 'Balanced long-context conversational model.', input: ['text'], category: 'general', recommended: false,
|
|
146
142
|
}),
|
|
147
143
|
createCuratedModel({
|
|
148
144
|
id: 'cline-pass/qwen3.7-max',
|
|
149
145
|
name: 'Qwen 3.7 Max',
|
|
150
|
-
description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
|
|
151
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: true,
|
|
146
|
+
description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.', input: ['text', 'image'], category: 'multimodal', recommended: true,
|
|
152
147
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
153
148
|
}),
|
|
154
149
|
createCuratedModel({
|
|
155
150
|
id: 'cline-pass/qwen3.7-plus',
|
|
156
151
|
name: 'Qwen 3.7 Plus',
|
|
157
|
-
description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
|
|
158
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'multimodal', recommended: false,
|
|
152
|
+
description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.', input: ['text', 'image'], category: 'multimodal', recommended: false,
|
|
159
153
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
160
154
|
}),
|
|
161
155
|
createCuratedModel({
|
|
162
156
|
id: 'cline-pass/minimax-m3',
|
|
163
157
|
name: 'MiniMax M3',
|
|
164
|
-
description: 'High throughput reasoning and synthesis model.',
|
|
165
|
-
ctx: 200000, maxTokens: 8192, input: ['text'], category: 'reasoning', recommended: false,
|
|
158
|
+
description: 'High throughput reasoning and synthesis model.', input: ['text'], category: 'reasoning', recommended: false,
|
|
166
159
|
reasoningEfforts: ['low', 'medium', 'high'],
|
|
167
160
|
}),
|
|
168
161
|
createCuratedModel({
|
|
169
162
|
id: 'cline-pass/mimo-v2.5',
|
|
170
163
|
name: 'MiMo V2.5',
|
|
171
|
-
description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
|
|
172
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'general', recommended: false,
|
|
164
|
+
description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.', input: ['text', 'image'], category: 'general', recommended: false,
|
|
173
165
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
174
166
|
}),
|
|
175
167
|
createCuratedModel({
|
|
176
168
|
id: 'cline-pass/mimo-v2.5-pro',
|
|
177
169
|
name: 'MiMo V2.5 Pro',
|
|
178
|
-
description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
|
|
179
|
-
ctx: 128000, maxTokens: 8192, input: ['text', 'image'], category: 'coding', recommended: false,
|
|
170
|
+
description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.', input: ['text', 'image'], category: 'coding', recommended: false,
|
|
180
171
|
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
181
172
|
}),
|
|
182
173
|
])
|
|
@@ -285,7 +276,7 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
285
276
|
*
|
|
286
277
|
* Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
|
|
287
278
|
*/
|
|
288
|
-
export function getAllModels(dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
279
|
+
export function getAllModels(dynamicModels = [], modelContextOverrides = [], customModels = [], options = {}) {
|
|
289
280
|
const normalizedDynamic = (Array.isArray(dynamicModels) ? dynamicModels : []).map(migrateModelEntry)
|
|
290
281
|
const overridesMap = new Map()
|
|
291
282
|
if (Array.isArray(modelContextOverrides)) {
|
|
@@ -328,6 +319,7 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
|
|
|
328
319
|
}
|
|
329
320
|
}
|
|
330
321
|
|
|
322
|
+
const dynamicIdSet = new Set(normalizedDynamic.map((m) => m.id))
|
|
331
323
|
let baseModels = []
|
|
332
324
|
if (normalizedDynamic.length > 0) {
|
|
333
325
|
const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
|
|
@@ -348,26 +340,41 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
|
|
|
348
340
|
originalContextLength: curated.originalContextLength || getOriginalModelContext(curated.id),
|
|
349
341
|
originalMaxTokens: curated.originalMaxTokens || getOriginalModelMaxTokens(curated.id),
|
|
350
342
|
originalProvider: curated.originalProvider || getOriginalModelProvider(curated.id),
|
|
351
|
-
defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens ||
|
|
343
|
+
defaultMaxTokens: curated.defaultMaxTokens || curated.maxTokens || getOriginalModelMaxTokens(curated.id),
|
|
352
344
|
input: dm.input && dm.input.length ? dm.input : curated.input,
|
|
353
345
|
category: dm.category || curated.category,
|
|
354
346
|
reasoningEfforts: dm.reasoningEfforts !== undefined ? dm.reasoningEfforts : curated.reasoningEfforts,
|
|
347
|
+
inPlan: true,
|
|
355
348
|
unverified: false,
|
|
356
349
|
})
|
|
357
350
|
}
|
|
351
|
+
const dynOrigCtx = getOriginalModelContext(dm.id)
|
|
352
|
+
const dynOrigMax = getOriginalModelMaxTokens(dm.id)
|
|
358
353
|
return applyOverride({
|
|
359
354
|
...dm,
|
|
360
|
-
clineContextLength: dm.contextLength ||
|
|
361
|
-
defaultContextLength: dm.contextLength ||
|
|
362
|
-
originalContextLength: dm.originalContextLength ||
|
|
363
|
-
originalMaxTokens: dm.originalMaxTokens ||
|
|
355
|
+
clineContextLength: dm.contextLength || dynOrigCtx,
|
|
356
|
+
defaultContextLength: dm.contextLength || dynOrigCtx,
|
|
357
|
+
originalContextLength: dm.originalContextLength || dynOrigCtx,
|
|
358
|
+
originalMaxTokens: dm.originalMaxTokens || dynOrigMax,
|
|
364
359
|
originalProvider: dm.originalProvider || getOriginalModelProvider(dm.id),
|
|
365
|
-
defaultMaxTokens: dm.maxTokens ||
|
|
360
|
+
defaultMaxTokens: dm.maxTokens || dynOrigMax,
|
|
361
|
+
contextLength: dm.contextLength || dynOrigCtx,
|
|
362
|
+
maxTokens: dm.maxTokens || dynOrigMax,
|
|
363
|
+
inPlan: true,
|
|
366
364
|
unverified: false,
|
|
367
365
|
})
|
|
368
366
|
})
|
|
367
|
+
|
|
368
|
+
if (options && options.includeOutOfPlan) {
|
|
369
|
+
const baseModelIds = new Set(baseModels.map((m) => m.id))
|
|
370
|
+
for (const cm of CLINE_MODELS) {
|
|
371
|
+
if (!baseModelIds.has(cm.id) && !dynamicIdSet.has(cm.id)) {
|
|
372
|
+
baseModels.push(applyOverride({ ...cm, inPlan: false, unverified: false }))
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
369
376
|
} else {
|
|
370
|
-
baseModels = CLINE_MODELS.map((m) => applyOverride({ ...m, unverified: true }))
|
|
377
|
+
baseModels = CLINE_MODELS.map((m) => applyOverride({ ...m, inPlan: true, unverified: true }))
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
if (Array.isArray(customModels) && customModels.length > 0) {
|
|
@@ -383,6 +390,7 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = [], cus
|
|
|
383
390
|
input: Array.isArray(cm.input) && cm.input.length ? cm.input : ['text'],
|
|
384
391
|
category: cm.category || 'general',
|
|
385
392
|
isCustom: true,
|
|
393
|
+
inPlan: true,
|
|
386
394
|
reasoningEfforts: cm.reasoningEfforts !== undefined ? cm.reasoningEfforts : (cm.isReasoning ? ['low', 'medium', 'high', 'max'] : undefined),
|
|
387
395
|
unverified: false,
|
|
388
396
|
}
|
package/lib/proxy-token.js
CHANGED
|
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
|
|
|
14
14
|
let cachedToken = null
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
|
-
let packageVersion = '0.5.
|
|
17
|
+
let packageVersion = '0.5.28'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.28'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
package/lib/routes/models.js
CHANGED
|
@@ -274,4 +274,41 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
274
274
|
}
|
|
275
275
|
},
|
|
276
276
|
}), 'dsh-clinebot: /models/probe')
|
|
277
|
+
// POST /dsh-clinebot/models/visible — configure visible models and plan filtering
|
|
278
|
+
ctx.effect(() => ctx.webServer.register({
|
|
279
|
+
kind: 'exact',
|
|
280
|
+
path: '/dsh-clinebot/models/visible',
|
|
281
|
+
handler: async (req, res) => {
|
|
282
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
283
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
284
|
+
try {
|
|
285
|
+
const bodyBuf = await readBody(req)
|
|
286
|
+
let body = {}
|
|
287
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
|
|
288
|
+
const settingsApi = getSettingsApi()
|
|
289
|
+
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
290
|
+
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
291
|
+
}
|
|
292
|
+
const current = plainConfig(live())
|
|
293
|
+
const next = { ...current }
|
|
294
|
+
if (Array.isArray(body.visibleModels)) {
|
|
295
|
+
next.visibleModels = body.visibleModels
|
|
296
|
+
}
|
|
297
|
+
if (typeof body.filterModelsByPlan === 'boolean') {
|
|
298
|
+
next.filterModelsByPlan = body.filterModelsByPlan
|
|
299
|
+
}
|
|
300
|
+
await settingsApi.replace(next)
|
|
301
|
+
await syncProviderState(next)
|
|
302
|
+
writeJson(res, 200, {
|
|
303
|
+
ok: true,
|
|
304
|
+
visibleModels: next.visibleModels,
|
|
305
|
+
filterModelsByPlan: next.filterModelsByPlan,
|
|
306
|
+
enabledModels: publicConfig(next).enabledModels,
|
|
307
|
+
})
|
|
308
|
+
} catch (err) {
|
|
309
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
}), 'dsh-clinebot: /models/visible')
|
|
313
|
+
|
|
277
314
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.28",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|