@goodandready/dsh-clinebot 0.3.4 → 0.3.5

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 CHANGED
@@ -5,6 +5,13 @@ 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.3.5] - 2026-09-09
9
+
10
+ ### Fixed
11
+ - **Settings Card React Crash & ErrorBoundary Isolation**: Wrapped `PluginCard` and `SettingsPage` with a defensive `ErrorBoundary` preventing unhandled render exceptions from unmounting the card.
12
+ - **useSyncExternalStore Stability**: Eliminated object allocations inside `getSnapshot` callback when `settingsScope` is detached or resolving, resolving infinite render loops (`Maximum update depth exceeded`).
13
+ - **Resilient Context & Reset Time Formatting**: Guaranteed fallback to module `ctx` inside `PluginCard` and added safe parsing for high-precision ISO timestamps in rolling quota progress bars.
14
+
8
15
  ## [0.3.4] - 2026-09-08
9
16
 
10
17
  ### Fixed
package/lib/client.js CHANGED
@@ -261,13 +261,70 @@ window.__ModuleLoader__.load({
261
261
  document.head.appendChild(style)
262
262
  }
263
263
 
264
+ function createErrorBoundary() {
265
+ if (!React || typeof React.Component !== 'function') {
266
+ return function NoopBoundary(props) { return props?.children || null }
267
+ }
268
+ return class ErrorBoundary extends React.Component {
269
+ constructor(props) {
270
+ super(props)
271
+ this.state = { hasError: false, error: null }
272
+ }
273
+ static getDerivedStateFromError(error) {
274
+ return { hasError: true, error }
275
+ }
276
+ componentDidCatch(error, errorInfo) {
277
+ console.error('[dsh-clinebot] React Error:', error, errorInfo)
278
+ }
279
+ render() {
280
+ if (this.state.hasError) {
281
+ return React.createElement(
282
+ 'div',
283
+ {
284
+ className: 'cb-alert cb-alert-err',
285
+ style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
286
+ },
287
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ClineBot UI Error:'),
288
+ React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
289
+ React.createElement(
290
+ 'button',
291
+ {
292
+ type: 'button',
293
+ className: 'cb-btn',
294
+ style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
295
+ onClick: () => this.setState({ hasError: false, error: null }),
296
+ },
297
+ 'Retry'
298
+ )
299
+ )
300
+ }
301
+ return this.props.children
302
+ }
303
+ }
304
+ }
305
+ const ErrorBoundary = createErrorBoundary()
306
+
307
+ const SNAPSHOT_READY = Object.freeze({ status: 'ready', value: {} })
308
+ const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', value: {} })
309
+
310
+ function formatResetTime(isoString) {
311
+ if (!isoString) return '—'
312
+ try {
313
+ const d = new Date(isoString)
314
+ if (isNaN(d.getTime())) return '—'
315
+ return d.toLocaleTimeString()
316
+ } catch {
317
+ return '—'
318
+ }
319
+ }
320
+
264
321
  function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
265
322
  const used = Math.max(0, Math.min(100, percentUsed || 0))
266
323
  let fillColor = 'var(--dsw-alias-state-success-primary)'
267
324
  if (used > 75) fillColor = 'var(--dsw-alias-state-warning-primary)'
268
325
  if (used > 90) fillColor = 'var(--dsw-alias-state-error-primary)'
269
326
 
270
- const resetStr = resetsAt ? new Date(resetsAt).toLocaleString() : '—'
327
+ const resetStr = formatResetTime(resetsAt)
271
328
 
272
329
  return React.createElement(
273
330
  'div',
@@ -299,15 +356,39 @@ window.__ModuleLoader__.load({
299
356
  const ctx = props?.ctx
300
357
  const t = props?.t || makeT(ru, en)
301
358
 
302
- const scope = React.useMemo(
303
- () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
304
- [ctx]
305
- )
359
+ const scope = React.useMemo(() => {
360
+ if (!ctx?.settingsScope?.bind) return undefined
361
+ try {
362
+ return ctx.settingsScope.bind({ namespace: NS })
363
+ } catch (_) {
364
+ return undefined
365
+ }
366
+ }, [ctx])
367
+
368
+ const subscribe = React.useMemo(() => {
369
+ return (cb) => {
370
+ if (!scope?.subscribe) return () => {}
371
+ try {
372
+ return scope.subscribe(cb) || (() => {})
373
+ } catch (_) {
374
+ return () => {}
375
+ }
376
+ }
377
+ }, [scope])
378
+
379
+ const getSnapshot = React.useCallback(() => {
380
+ if (!scope?.getSnapshot) return SNAPSHOT_READY
381
+ try {
382
+ return scope.getSnapshot() || SNAPSHOT_READY
383
+ } catch (_) {
384
+ return SNAPSHOT_READY
385
+ }
386
+ }, [scope])
306
387
 
307
388
  const snapshot = React.useSyncExternalStore(
308
- React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
309
- React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
310
- React.useCallback(() => ({ status: 'loading' }), [])
389
+ subscribe,
390
+ getSnapshot,
391
+ React.useCallback(() => SNAPSHOT_LOADING, [])
311
392
  )
312
393
  const snapshotStatus = snapshot?.status || 'ready'
313
394
 
@@ -1094,7 +1175,17 @@ window.__ModuleLoader__.load({
1094
1175
  React.createElement(Chevron)
1095
1176
  )
1096
1177
  ),
1097
- open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage, { ...props, ctx: props?.ctx })) : null
1178
+ open
1179
+ ? React.createElement(
1180
+ 'div',
1181
+ { style: { marginTop: '16px' } },
1182
+ React.createElement(
1183
+ ErrorBoundary,
1184
+ null,
1185
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx })
1186
+ )
1187
+ )
1188
+ : null
1098
1189
  )
1099
1190
  }
1100
1191
 
@@ -1106,7 +1197,8 @@ window.__ModuleLoader__.load({
1106
1197
 
1107
1198
  let placed = false
1108
1199
  try {
1109
- placed = !!ctx.slots.inject('settings.plugin.item', () => {
1200
+ const res = ctx.slots.inject('settings.plugin.item', () => {
1201
+ placed = true
1110
1202
  return ctx.slots.register(
1111
1203
  {
1112
1204
  name: 'settings.plugin.item',
@@ -1114,9 +1206,10 @@ window.__ModuleLoader__.load({
1114
1206
  locale: NS,
1115
1207
  inject: () => ({ ctx }),
1116
1208
  },
1117
- PluginCard
1209
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1118
1210
  )
1119
1211
  })
1212
+ if (res === null) placed = false
1120
1213
  } catch (_) {
1121
1214
  placed = false
1122
1215
  }
@@ -1134,7 +1227,7 @@ window.__ModuleLoader__.load({
1134
1227
  label: () => t('title'),
1135
1228
  inject: () => ({ ctx }),
1136
1229
  },
1137
- SettingsPage
1230
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx }))
1138
1231
  )
1139
1232
  })
1140
1233
  } catch (_) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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",