@goodandready/dsh-clinebot 0.3.4 → 0.3.6

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/client.js +128 -12
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ 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.6] - 2026-09-09
9
+
10
+ ### Fixed
11
+ - **Restored `handleSmoke` diagnostics handler**: Re-introduced missing `handleSmoke` click callback in `SettingsPage`, fixing `handleSmoke is not defined` runtime error during Diagnostics card interaction.
12
+ - **Defensive ErrorBoundary child rendering**: Hardened `ErrorBoundary.render()` with `this.props?.children || null` to prevent unhandled exceptions if props are omitted.
13
+ - **Comprehensive UI render integrity test**: Added recursive VDOM component tree evaluation test in `test/client-modules.test.js` covering all 190+ elements and verifying definition of all 18 event handlers.
14
+
15
+ ## [0.3.5] - 2026-09-09
16
+
17
+ ### Fixed
18
+ - **Settings Card React Crash & ErrorBoundary Isolation**: Wrapped `PluginCard` and `SettingsPage` with a defensive `ErrorBoundary` preventing unhandled render exceptions from unmounting the card.
19
+ - **useSyncExternalStore Stability**: Eliminated object allocations inside `getSnapshot` callback when `settingsScope` is detached or resolving, resolving infinite render loops (`Maximum update depth exceeded`).
20
+ - **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.
21
+
8
22
  ## [0.3.4] - 2026-09-08
9
23
 
10
24
  ### 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 || null
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
 
@@ -445,6 +526,29 @@ window.__ModuleLoader__.load({
445
526
  }
446
527
  }
447
528
 
529
+ // Smoke chat test
530
+ async function handleSmoke() {
531
+ setBusy('smoke')
532
+ setErr('')
533
+ setMsg('')
534
+ setSmokeResult(null)
535
+ try {
536
+ const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
537
+ method: 'POST',
538
+ headers: { 'Content-Type': 'application/json' },
539
+ body: JSON.stringify({ model: draft?.defaultModel }),
540
+ })
541
+ const data = await res.json().catch(() => ({}))
542
+ if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
543
+ setSmokeResult(data)
544
+ setMsg(t('diag.smoke_ok', { latency: data.latencyMs }))
545
+ } catch (e) {
546
+ setErr(String(e.message || e))
547
+ } finally {
548
+ setBusy('')
549
+ }
550
+ }
551
+
448
552
  // Fast Browser Login
449
553
  async function handleFastLogin() {
450
554
  setBusy('fast-login')
@@ -1094,7 +1198,17 @@ window.__ModuleLoader__.load({
1094
1198
  React.createElement(Chevron)
1095
1199
  )
1096
1200
  ),
1097
- open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage, { ...props, ctx: props?.ctx })) : null
1201
+ open
1202
+ ? React.createElement(
1203
+ 'div',
1204
+ { style: { marginTop: '16px' } },
1205
+ React.createElement(
1206
+ ErrorBoundary,
1207
+ null,
1208
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx })
1209
+ )
1210
+ )
1211
+ : null
1098
1212
  )
1099
1213
  }
1100
1214
 
@@ -1106,7 +1220,8 @@ window.__ModuleLoader__.load({
1106
1220
 
1107
1221
  let placed = false
1108
1222
  try {
1109
- placed = !!ctx.slots.inject('settings.plugin.item', () => {
1223
+ const res = ctx.slots.inject('settings.plugin.item', () => {
1224
+ placed = true
1110
1225
  return ctx.slots.register(
1111
1226
  {
1112
1227
  name: 'settings.plugin.item',
@@ -1114,9 +1229,10 @@ window.__ModuleLoader__.load({
1114
1229
  locale: NS,
1115
1230
  inject: () => ({ ctx }),
1116
1231
  },
1117
- PluginCard
1232
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1118
1233
  )
1119
1234
  })
1235
+ if (res === null) placed = false
1120
1236
  } catch (_) {
1121
1237
  placed = false
1122
1238
  }
@@ -1134,7 +1250,7 @@ window.__ModuleLoader__.load({
1134
1250
  label: () => t('title'),
1135
1251
  inject: () => ({ ctx }),
1136
1252
  },
1137
- SettingsPage
1253
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx }))
1138
1254
  )
1139
1255
  })
1140
1256
  } 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.6",
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",