@goodandready/dsh-clinebot 0.5.8 → 0.5.10

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
@@ -24,6 +24,19 @@ function readConfigForms(ctx) {
24
24
  try { return ctx.get('configForms') || undefined } catch { return undefined }
25
25
  }
26
26
 
27
+ function useConfigFormsSnapshot(ctx) {
28
+ const scope = React.useMemo(() => {
29
+ try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
30
+ }, [ctx])
31
+ const subscribe = React.useMemo(() => (cb) => {
32
+ try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
33
+ }, [scope])
34
+ const getSnapshot = React.useCallback(() => {
35
+ try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
36
+ }, [scope])
37
+ return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
38
+ }
39
+
27
40
  // --- src/client/locales.js ---
28
41
  const en = {
29
42
  title: 'ClineBot',
@@ -79,6 +92,28 @@ const en = {
79
92
  'quota.window_weekly': '📅 Weekly Window',
80
93
  'quota.used': '{pct}% used',
81
94
  'quota.reset_at': 'Reset: {time}',
95
+ 'quota.window_monthly': '📅 Monthly Window',
96
+ 'quota.countdown': 'Resets in {time}',
97
+ 'quota.resetting': 'Resetting…',
98
+ 'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
99
+ 'quota.exhausted_5h': '5-hour rolling limit is almost exhausted ({percentUsed}%). New requests may be rejected until quota reset.',
100
+ 'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
101
+ 'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
102
+ 'quota.warning_monthly': 'Low monthly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
103
+ 'models.reasoning_tag': '🧠 Reasoning',
104
+ 'models.effort_low': 'Low',
105
+ 'models.effort_medium': 'Medium',
106
+ 'models.effort_high': 'High',
107
+ 'models.effort_max': 'Max',
108
+ 'models.caps_vision': 'Vision',
109
+ 'models.caps_general': 'General',
110
+ 'models.caps_coding': 'Coding',
111
+ 'diag.smoke_result': '✅ Latency: {latency} ms | Model: {model}\nResponse: {preview}',
112
+ 'diag.empty': '(empty)',
113
+ 'ui.error_title': '⚠️ ClineBot UI Error: ',
114
+ 'ui.retry': 'Retry',
115
+ 'update.check_failed': 'Failed to check for updates',
116
+ 'update.retry': 'Retry check',
82
117
  'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
83
118
  '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.',
84
119
  'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
@@ -206,6 +241,28 @@ const en = {
206
241
  'quota.window_weekly': '📅 每周滚动窗口',
207
242
  'quota.used': '已使用 {pct}%',
208
243
  'quota.reset_at': '重置时间:{time}',
244
+ 'quota.window_monthly': '📅 每月滚动窗口',
245
+ 'quota.countdown': '{time}后重置',
246
+ 'quota.resetting': '正在重置',
247
+ 'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
248
+ 'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
249
+ 'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
250
+ 'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
251
+ 'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
252
+ 'models.reasoning_tag': '🧠 深度思考',
253
+ 'models.effort_low': '低',
254
+ 'models.effort_medium': '中',
255
+ 'models.effort_high': '高',
256
+ 'models.effort_max': '最高',
257
+ 'models.caps_vision': '视觉',
258
+ 'models.caps_general': '通用',
259
+ 'models.caps_coding': '编程',
260
+ 'diag.smoke_result': '✅ 延迟:{latency} ms | 模型:{model}\n响应:{preview}',
261
+ 'diag.empty': '(空)',
262
+ 'ui.error_title': '⚠️ ClineBot 界面错误:',
263
+ 'ui.retry': '重试',
264
+ 'update.check_failed': '检查更新失败',
265
+ 'update.retry': '重试检查',
209
266
  'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
210
267
  'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
211
268
  'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
@@ -281,6 +338,9 @@ const en = {
281
338
 
282
339
  function makeT(dict, fallback) {
283
340
  return function t(key, vars) {
341
+ if (key === 'quota.countdown' && vars?.time === 'resetting') {
342
+ return (dict && dict['quota.resetting']) || (fallback && fallback['quota.resetting']) || '正在重置'
343
+ }
284
344
  let val = (dict && dict[key]) || (fallback && fallback[key]) || key
285
345
  if (vars && typeof val === 'string') {
286
346
  for (const k of Object.keys(vars)) {
@@ -399,10 +459,13 @@ function createErrorBoundary() {
399
459
  }
400
460
  render() {
401
461
  if (this.state.hasError) {
462
+ const t = this.props && typeof this.props.t === 'function' ? this.props.t : null
463
+ const errorTitle = t ? t('ui.error_title') : '⚠️ ClineBot UI Error: '
464
+ const retryLabel = t ? t('ui.retry') : 'Retry'
402
465
  return React.createElement(
403
466
  'div',
404
467
  { className: 'cb-alert-err' },
405
- React.createElement('strong', null, '⚠️ ClineBot UI Error: '),
468
+ React.createElement('strong', null, errorTitle),
406
469
  String(this.state.error?.message || this.state.error || 'Unknown rendering error'),
407
470
  React.createElement(
408
471
  'button',
@@ -412,7 +475,7 @@ function createErrorBoundary() {
412
475
  style: { marginLeft: '12px', padding: '2px 8px', fontSize: '11px' },
413
476
  onClick: () => this.setState({ hasError: false, error: null }),
414
477
  },
415
- 'Retry'
478
+ retryLabel
416
479
  )
417
480
  )
418
481
  }
@@ -512,7 +575,7 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
512
575
  },
513
576
  },
514
577
  t('quota.used', { pct }),
515
- resetCountdown ? ` · ${t('quota.countdown', { time: resetCountdown })}` : (resetClock ? ` · ${t('quota.reset_at', { time: resetClock })}` : '')
578
+ resetCountdown ? ` · ${resetCountdown === 'resetting' ? (t('quota.resetting') || '正在重置') : t('quota.countdown', { time: resetCountdown })}` : (resetClock ? ` · ${t('quota.reset_at', { time: resetClock })}` : '')
516
579
  )
517
580
  ),
518
581
  React.createElement(
@@ -537,7 +600,22 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
537
600
  }
538
601
 
539
602
  // --- src/client/components/update-banner.js ---
540
- function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
603
+ function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }) {
604
+ const status = updateState.status || (
605
+ updateState.checking
606
+ ? 'checking'
607
+ : (updateState.latestCheckFailed
608
+ ? 'error'
609
+ : (updateState.updateAvailable
610
+ ? 'available'
611
+ : (updateState.lastCheckedAt ? 'current' : 'idle')))
612
+ )
613
+
614
+ const isChecking = status === 'checking' || updateState.checking
615
+ const isAvailable = status === 'available' || updateState.updateAvailable
616
+ const isCurrent = status === 'current'
617
+ const isError = status === 'error' || updateState.latestCheckFailed
618
+
541
619
  return React.createElement(
542
620
  React.Fragment,
543
621
  null,
@@ -561,19 +639,25 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
561
639
  `v${updateState.currentVersion}`
562
640
  )
563
641
  : null,
564
- updateState.checking
642
+ isChecking
565
643
  ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
566
644
  t('update.checking')
567
645
  )
568
- : updateState.updateAvailable
646
+ : isAvailable
569
647
  ? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
570
648
  t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
571
649
  )
572
- : React.createElement('span', { className: 'cb-badge cb-badge-ok' },
573
- '✓ ' + t('update.up_to_date')
574
- )
650
+ : isCurrent
651
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok' },
652
+ '✓ ' + t('update.up_to_date')
653
+ )
654
+ : isError
655
+ ? React.createElement('span', { className: 'cb-badge cb-badge-bad' },
656
+ t('update.check_failed')
657
+ )
658
+ : null
575
659
  ),
576
- updateState.updateAvailable
660
+ isAvailable
577
661
  ? React.createElement(
578
662
  'button',
579
663
  {
@@ -585,7 +669,19 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
585
669
  },
586
670
  updateState.updating ? t('update.updating') : t('update.btn')
587
671
  )
588
- : null
672
+ : isError && handleCheckUpdate
673
+ ? React.createElement(
674
+ 'button',
675
+ {
676
+ type: 'button',
677
+ className: 'cb-btn',
678
+ disabled: isChecking,
679
+ onClick: handleCheckUpdate,
680
+ style: { padding: '5px 12px', fontSize: '12px' }
681
+ },
682
+ t('update.retry')
683
+ )
684
+ : null
589
685
  ),
590
686
  updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice) : null,
591
687
  updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
@@ -936,7 +1032,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
936
1032
  React.createElement(
937
1033
  'div',
938
1034
  { style: { flex: 1 } },
939
- React.createElement('div', null, status.quotaWarning.message),
1035
+ React.createElement('div', null, (status.quotaWarning.code && t(status.quotaWarning.code, status.quotaWarning.params)) || status.quotaWarning.message),
940
1036
  status.quotaWarning.resetsAt
941
1037
  ? React.createElement(
942
1038
  'div',
@@ -1552,19 +1648,20 @@ function ModelsSection({
1552
1648
  m.category === 'reasoning'
1553
1649
  )
1554
1650
  const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
1651
+ const categoryLabel = category === 'general' ? t('models.caps_general') : (category === 'coding' ? t('models.caps_coding') : category)
1555
1652
  const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
1556
1653
  return React.createElement(
1557
1654
  React.Fragment,
1558
1655
  null,
1559
- category ? React.createElement('span', { className: 'cb-badge' }, category) : null,
1656
+ category ? React.createElement('span', { className: 'cb-badge' }, categoryLabel) : null,
1560
1657
  hasVision
1561
- ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, 'Vision')
1658
+ ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, t('models.caps_vision'))
1562
1659
  : null,
1563
1660
  hasReasoning
1564
1661
  ? React.createElement(
1565
1662
  React.Fragment,
1566
1663
  null,
1567
- React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '🧠 Reasoning'),
1664
+ React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, t('models.reasoning_tag')),
1568
1665
  React.createElement(
1569
1666
  'select',
1570
1667
  {
@@ -1579,10 +1676,10 @@ function ModelsSection({
1579
1676
  },
1580
1677
  },
1581
1678
  React.createElement('option', { value: '' }, t('models.effort_auto')),
1582
- React.createElement('option', { value: 'low' }, 'Low'),
1583
- React.createElement('option', { value: 'medium' }, 'Medium'),
1584
- React.createElement('option', { value: 'high' }, 'High'),
1585
- React.createElement('option', { value: 'max' }, 'Max')
1679
+ React.createElement('option', { value: 'low' }, t('models.effort_low')),
1680
+ React.createElement('option', { value: 'medium' }, t('models.effort_medium')),
1681
+ React.createElement('option', { value: 'high' }, t('models.effort_high')),
1682
+ React.createElement('option', { value: 'max' }, t('models.effort_max'))
1586
1683
  )
1587
1684
  )
1588
1685
  : null
@@ -1785,27 +1882,17 @@ function DiagSection({ keyPresent, isRegistered, busy, smokeResult, handleSmoke,
1785
1882
  ? React.createElement(
1786
1883
  'div',
1787
1884
  { className: 'cb-preview' },
1788
- `✅ Latency: ${smokeResult.latencyMs} ms | Model: ${smokeResult.model}
1789
- Response: ${smokeResult.preview || '(empty)'}`
1885
+ t('diag.smoke_result', {
1886
+ latency: smokeResult.latencyMs,
1887
+ model: smokeResult.model,
1888
+ preview: smokeResult.preview || t('diag.empty'),
1889
+ })
1790
1890
  )
1791
1891
  : null
1792
1892
  )
1793
1893
  }
1794
1894
 
1795
1895
  // --- src/client/settings-page.js ---
1796
- function useConfigFormsSnapshot(ctx) {
1797
- const scope = React.useMemo(() => {
1798
- try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
1799
- }, [ctx])
1800
- const subscribe = React.useMemo(() => (cb) => {
1801
- try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
1802
- }, [scope])
1803
- const getSnapshot = React.useCallback(() => {
1804
- try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
1805
- }, [scope])
1806
- return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
1807
- }
1808
-
1809
1896
  function SettingsPage(props) {
1810
1897
  const ctx = props?.ctx
1811
1898
  const t = props?.t || makeT(en, en)
@@ -1819,8 +1906,9 @@ function SettingsPage(props) {
1819
1906
  const [smokeResult, setSmokeResult] = React.useState(null)
1820
1907
 
1821
1908
  const [updateState, setUpdateState] = React.useState({
1822
- checking: false, updating: false, currentVersion: '', latestVersion: '',
1823
- updateAvailable: false, canAutoUpdate: true, error: '', notice: '',
1909
+ status: 'idle', checking: false, updating: false, currentVersion: '', latestVersion: '',
1910
+ updateAvailable: false, canAutoUpdate: true, latestCheckFailed: false, lastCheckedAt: null,
1911
+ error: '', notice: '',
1824
1912
  })
1825
1913
 
1826
1914
  const [apiKeyInput, setApiKeyInput] = React.useState('')
@@ -1847,23 +1935,45 @@ function SettingsPage(props) {
1847
1935
  }, [load])
1848
1936
 
1849
1937
  const checkUpdate = React.useCallback(async () => {
1850
- setUpdateState((s) => ({ ...s, checking: true, error: '' }))
1938
+ setUpdateState((s) => ({ ...s, checking: true, status: 'checking', error: '' }))
1851
1939
  try {
1852
1940
  const res = await fetch(`${ROUTE_PREFIX}/update`)
1853
1941
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
1854
1942
  const data = await res.json().catch(() => ({}))
1943
+ if (data.latestCheckFailed) {
1944
+ setUpdateState((s) => ({
1945
+ ...s,
1946
+ checking: false,
1947
+ status: 'error',
1948
+ latestCheckFailed: true,
1949
+ currentVersion: data.currentVersion || s.currentVersion,
1950
+ error: t('update.check_failed'),
1951
+ }))
1952
+ return
1953
+ }
1954
+ const isAvailable = Boolean(data.updateAvailable)
1855
1955
  setUpdateState((s) => ({
1856
1956
  ...s,
1857
1957
  checking: false,
1958
+ status: isAvailable ? 'available' : 'current',
1858
1959
  currentVersion: data.currentVersion || s.currentVersion,
1859
1960
  latestVersion: data.latestVersion || '',
1860
- updateAvailable: !!data.updateAvailable,
1961
+ updateAvailable: isAvailable,
1962
+ latestCheckFailed: false,
1963
+ lastCheckedAt: Date.now(),
1861
1964
  canAutoUpdate: data.canAutoUpdate !== false,
1965
+ error: '',
1966
+ }))
1967
+ } catch (err) {
1968
+ setUpdateState((s) => ({
1969
+ ...s,
1970
+ checking: false,
1971
+ status: 'error',
1972
+ latestCheckFailed: true,
1973
+ error: String(err?.message || err || t('update.check_failed')),
1862
1974
  }))
1863
- } catch (_) {
1864
- setUpdateState((s) => ({ ...s, checking: false }))
1865
1975
  }
1866
- }, [])
1976
+ }, [t])
1867
1977
 
1868
1978
  React.useEffect(() => { checkUpdate() }, [checkUpdate])
1869
1979
 
@@ -1881,13 +1991,20 @@ function SettingsPage(props) {
1881
1991
  setUpdateState((s) => ({
1882
1992
  ...s,
1883
1993
  updating: false,
1994
+ status: 'current',
1884
1995
  updateAvailable: false,
1885
1996
  currentVersion: newVer,
1997
+ lastCheckedAt: Date.now(),
1886
1998
  notice: t('update.done', { version: newVer }),
1887
1999
  }))
1888
2000
  setTimeout(() => checkUpdate(), 2000)
1889
2001
  } catch (err) {
1890
- setUpdateState((s) => ({ ...s, updating: false, error: t('update.failed', { error: String(err.message || err) }) }))
2002
+ setUpdateState((s) => ({
2003
+ ...s,
2004
+ updating: false,
2005
+ status: s.updateAvailable ? 'available' : 'error',
2006
+ error: t('update.failed', { error: String(err?.message || err) }),
2007
+ }))
1891
2008
  }
1892
2009
  }
1893
2010
 
@@ -1909,8 +2026,7 @@ function SettingsPage(props) {
1909
2026
  if (!keyVal) { setErr(t('key.empty_err')); return }
1910
2027
  await performAction('save-key', async () => {
1911
2028
  const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
1912
- method: 'POST',
1913
- headers: { 'Content-Type': 'application/json' },
2029
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1914
2030
  body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: draft?.apiKeyEnv }),
1915
2031
  })
1916
2032
  const data = await res.json().catch(() => ({}))
@@ -1934,8 +2050,7 @@ function SettingsPage(props) {
1934
2050
  async function handleUpdateModelContext(payload) {
1935
2051
  await performAction('update-context', async () => {
1936
2052
  const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
1937
- method: 'POST',
1938
- headers: { 'Content-Type': 'application/json' },
2053
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1939
2054
  body: JSON.stringify(payload),
1940
2055
  })
1941
2056
  const data = await res.json().catch(() => ({}))
@@ -2004,8 +2119,7 @@ function SettingsPage(props) {
2004
2119
  setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
2005
2120
  try {
2006
2121
  const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
2007
- method: 'POST',
2008
- headers: { 'Content-Type': 'application/json' },
2122
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2009
2123
  body: JSON.stringify({ key: raw }),
2010
2124
  })
2011
2125
  const data = await res.json().catch(() => ({}))
@@ -2030,8 +2144,7 @@ function SettingsPage(props) {
2030
2144
  async function handlePinAccount(accountEnv) {
2031
2145
  await performAction('pin-account', async () => {
2032
2146
  const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
2033
- method: 'POST',
2034
- headers: { 'Content-Type': 'application/json' },
2147
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2035
2148
  body: JSON.stringify({ account: accountEnv }),
2036
2149
  })
2037
2150
  const data = await res.json().catch(() => ({}))
@@ -2043,8 +2156,7 @@ function SettingsPage(props) {
2043
2156
  async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
2044
2157
  await performAction('add-account', async () => {
2045
2158
  const res = await fetch(`${ROUTE_PREFIX}/accounts`, {
2046
- method: 'POST',
2047
- headers: { 'Content-Type': 'application/json' },
2159
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2048
2160
  body: JSON.stringify({ label, apiKeyEnv, apiKey }),
2049
2161
  })
2050
2162
  const data = await res.json().catch(() => ({}))
@@ -2057,8 +2169,7 @@ function SettingsPage(props) {
2057
2169
  async function handleDeleteAccount(accountEnv, deleteSecret = true) {
2058
2170
  await performAction('delete-account', async () => {
2059
2171
  const res = await fetch(`${ROUTE_PREFIX}/accounts/delete`, {
2060
- method: 'POST',
2061
- headers: { 'Content-Type': 'application/json' },
2172
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2062
2173
  body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
2063
2174
  })
2064
2175
  const data = await res.json().catch(() => ({}))
@@ -2071,8 +2182,7 @@ function SettingsPage(props) {
2071
2182
  setDraft((d) => ({ ...(d || {}), ...(patch || {}) }))
2072
2183
  await performAction('patch-config', async () => {
2073
2184
  const res = await fetch(`${ROUTE_PREFIX}/config`, {
2074
- method: 'PUT',
2075
- headers: { 'Content-Type': 'application/json' },
2185
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
2076
2186
  body: JSON.stringify({ config: patch }),
2077
2187
  })
2078
2188
  const data = await res.json().catch(() => ({}))
@@ -2136,15 +2246,10 @@ function SettingsPage(props) {
2136
2246
  function handleSetModelsFilter(type) {
2137
2247
  const all = status?.availableModels || []
2138
2248
  let allowed = new Set()
2139
- if (type === 'all') {
2140
- allowed = new Set(all.map((m) => m.id))
2141
- } else if (type === 'vision') {
2142
- allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
2143
- } else if (type === 'coding') {
2144
- allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
2145
- } else if (type === 'recommended') {
2146
- allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
2147
- }
2249
+ if (type === 'all') allowed = new Set(all.map((m) => m.id))
2250
+ else if (type === 'vision') allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
2251
+ else if (type === 'coding') allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
2252
+ else if (type === 'recommended') allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
2148
2253
 
2149
2254
  const curr = draft?.disabledModels || []
2150
2255
  const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
@@ -2205,7 +2310,7 @@ function SettingsPage(props) {
2205
2310
  ),
2206
2311
 
2207
2312
  // In-app Update Bar
2208
- React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
2313
+ React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
2209
2314
 
2210
2315
  // Settings host status warning
2211
2316
  snapshotStatus === 'unavailable'
@@ -2338,7 +2443,7 @@ function PluginCard(props) {
2338
2443
  { style: { marginTop: '16px' } },
2339
2444
  React.createElement(
2340
2445
  ErrorBoundary,
2341
- null,
2446
+ { t },
2342
2447
  React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
2343
2448
  )
2344
2449
  )
@@ -2423,7 +2528,7 @@ function apply(ctx) {
2423
2528
  locale: NS,
2424
2529
  inject: () => ({ ctx }),
2425
2530
  },
2426
- (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2531
+ (props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2427
2532
  )
2428
2533
  )
2429
2534
 
@@ -2440,7 +2545,7 @@ function apply(ctx) {
2440
2545
  locale: NS,
2441
2546
  inject: () => ({ ctx }),
2442
2547
  },
2443
- (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2548
+ (props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2444
2549
  )
2445
2550
  )
2446
2551
  }
@@ -2453,7 +2558,7 @@ function apply(ctx) {
2453
2558
  locale: NS,
2454
2559
  inject: () => ({ ctx }),
2455
2560
  },
2456
- (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2561
+ (props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
2457
2562
  )
2458
2563
  )
2459
2564
  }
@@ -528,10 +528,7 @@ export function buildPiAiProvider({
528
528
  if (validEfforts.includes(key)) efforts[key] = key === 'off' ? (v === undefined ? null : v) : (typeof v === 'string' && v.trim() ? v.trim() : key)
529
529
  }
530
530
  }
531
- const modelCompat = {
532
- sendSessionAffinityHeaders: true,
533
- sessionAffinityFormat: 'openrouter',
534
- }
531
+ const modelCompat = {}
535
532
  if (efforts && Object.keys(efforts).length) {
536
533
  if (!('off' in efforts)) efforts.off = null
537
534
  res.reasoningEfforts = efforts
@@ -539,7 +536,9 @@ export function buildPiAiProvider({
539
536
  } else {
540
537
  res.reasoningEfforts = false
541
538
  }
542
- res.compat = modelCompat
539
+ if (Object.keys(modelCompat).length > 0) {
540
+ res.compat = modelCompat
541
+ }
543
542
  return res
544
543
  })
545
544
 
@@ -550,8 +549,6 @@ export function buildPiAiProvider({
550
549
  apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
551
550
  compat: {
552
551
  supportsReasoningEffort: true,
553
- sendSessionAffinityHeaders: true,
554
- sessionAffinityFormat: 'openrouter',
555
552
  },
556
553
  models: modelList,
557
554
  }
package/lib/index.js CHANGED
@@ -22,7 +22,7 @@ import { registerModelsRoutes } from './routes/models.js'
22
22
  import { registerAuthRoutes } from './routes/auth.js'
23
23
  import { registerProxyRoutes } from './routes/proxy.js'
24
24
  import { registerSlashCommand } from './slash-command.js'
25
- import { recordUsage, loadStats, flushStats } from './stats-storage.js'
25
+ import { recordUsage, loadStats, flushStats, resolvePath } from './stats-storage.js'
26
26
 
27
27
  export const name = '@goodandready/dsh-clinebot'
28
28
  export const inject = ['webServer', 'credentials']
@@ -35,7 +35,41 @@ export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSess
35
35
 
36
36
  export function apply(ctx, config) {
37
37
  let currentConfig = config
38
- let getConfig = () => currentConfig
38
+ const overrides = {}
39
+ const getConfig = () => {
40
+ if (!currentConfig) return currentConfig
41
+ if (Object.isFrozen(currentConfig)) {
42
+ return new Proxy({}, {
43
+ get(_target, prop) {
44
+ const orig = Reflect.get(currentConfig, prop)
45
+ if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(currentConfig, prop)?.get) {
46
+ return orig
47
+ }
48
+ if (prop in overrides) {
49
+ return overrides[prop]
50
+ }
51
+ return orig
52
+ },
53
+ has(_target, prop) {
54
+ return prop in overrides || Reflect.has(currentConfig, prop)
55
+ },
56
+ ownKeys() {
57
+ return Array.from(new Set([...Reflect.ownKeys(currentConfig), ...Object.keys(overrides)]))
58
+ },
59
+ getOwnPropertyDescriptor(_target, prop) {
60
+ if (prop in overrides) {
61
+ return { configurable: true, enumerable: true, value: overrides[prop], writable: true }
62
+ }
63
+ const desc = Object.getOwnPropertyDescriptor(currentConfig, prop)
64
+ if (desc) {
65
+ return { configurable: true, enumerable: desc.enumerable ?? true, value: Reflect.get(currentConfig, prop) }
66
+ }
67
+ return undefined
68
+ }
69
+ })
70
+ }
71
+ return currentConfig
72
+ }
39
73
  const live = () => (getConfig() ? Config(structuredClone(plainConfig(getConfig()))) : config)
40
74
  let settingsApi
41
75
 
@@ -59,6 +93,18 @@ export function apply(ctx, config) {
59
93
  autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
60
94
  }
61
95
 
96
+ let activeStatsPath = resolvePath(publicConfig(live()).statsPath)
97
+ loadStats(activeStatsPath)
98
+
99
+ const syncStatsStorage = (cfg) => {
100
+ const nextPath = resolvePath(publicConfig(cfg).statsPath)
101
+ if (nextPath !== activeStatsPath) {
102
+ flushStats(activeStatsPath)
103
+ activeStatsPath = nextPath
104
+ loadStats(nextPath)
105
+ }
106
+ }
107
+
62
108
  const getRevision = (svc) => {
63
109
  try {
64
110
  return svc?.describe?.().find((row) => row.ns === NS)?.revision
@@ -69,22 +115,37 @@ export function apply(ctx, config) {
69
115
 
70
116
  const applyConfigMutations = (target, source) => {
71
117
  const targetConfig = typeof target === 'function' ? target() : target
72
- if (targetConfig && typeof targetConfig === 'object' && !Object.isFrozen(targetConfig)) {
118
+ if (targetConfig && typeof targetConfig === 'object') {
119
+ if (Object.isFrozen(targetConfig)) {
120
+ for (const [key, value] of Object.entries(source)) {
121
+ const orig = targetConfig[key]
122
+ if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
123
+ continue
124
+ }
125
+ overrides[key] = value
126
+ }
127
+ return
128
+ }
73
129
  for (const [key, value] of Object.entries(source)) {
74
130
  try {
75
- if (isVolatileRef(targetConfig[key]) && typeof targetConfig[key].set === 'function') {
76
- targetConfig[key].set(value)
77
- } else if (Object.getOwnPropertyDescriptor(targetConfig, key)?.set) {
131
+ if (isVolatileRef(targetConfig[key])) {
132
+ // Cosmokit native Volatile references are reactive and authoritative;
133
+ // preserve the reference without overwriting with plain value.
134
+ continue
135
+ }
136
+ const desc = Object.getOwnPropertyDescriptor(targetConfig, key)
137
+ if (desc?.get && !desc.set) {
138
+ continue
139
+ }
140
+ if (desc?.set) {
78
141
  targetConfig[key] = value
79
- } else if (!Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
142
+ } else {
80
143
  targetConfig[key] = value
81
144
  }
82
145
  } catch {
83
146
  /* ignore read-only */
84
147
  }
85
148
  }
86
- } else {
87
- currentConfig = source
88
149
  }
89
150
  }
90
151
 
@@ -104,7 +165,7 @@ export function apply(ctx, config) {
104
165
  await svc.update(NS, payload, rev)
105
166
  }
106
167
  applyConfigMutations(currentConfig, parsed)
107
- loadStats(publicConfig(live()).statsPath)
168
+ syncStatsStorage(live())
108
169
  await syncProviderState(live())
109
170
  return parsed
110
171
  },
@@ -122,7 +183,7 @@ export function apply(ctx, config) {
122
183
  await svc.replace(NS, payload, rev)
123
184
  }
124
185
  applyConfigMutations(currentConfig, merged)
125
- loadStats(publicConfig(live()).statsPath)
186
+ syncStatsStorage(live())
126
187
  await syncProviderState(live())
127
188
  return merged
128
189
  },
@@ -133,11 +194,10 @@ export function apply(ctx, config) {
133
194
  },
134
195
  }
135
196
 
136
- loadStats(publicConfig(live()).statsPath)
137
197
  if (typeof ctx.effect === 'function') {
138
198
  ctx.effect(() => {
139
199
  return () => {
140
- flushStats(publicConfig(live()).statsPath)
200
+ flushStats(activeStatsPath)
141
201
  }
142
202
  }, 'dsh-clinebot: stats-storage')
143
203
  }