@goodandready/dsh-plugin-notify 0.4.0 → 0.4.1

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
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.1] - 2026-10-10
8
+
9
+ ### Fixed
10
+ - **Settings registration namespace & Desktop crash protection** (#55): Use canonical `ENTRY_ID = 'plugin-notify'` instead of scoped package name in `settings.register`; wrap call with `try / catch` logging warning instead of throwing `TypeError` in DSH Desktop.
11
+ - **Settings form slot binding** (#54): Bind UI `settings.plugin.item` slot `key` to `ROW_ID` (`'plugin-notify'`), and prioritize `ROW_ID` in `configForms` candidates over legacy `NS`.
12
+ - **Dynamic settings observation** (#48): Make describe settings fallback reactive: observe `loader/volatile-update`, `settings/document-updated`, and `settings/updated` to dynamically refresh settings when edited in UI or external config.
13
+ - **Config unboxing guarantee** (#47): Guarantee `getConfig()` always unwraps volatile boxes via `unwrapConfig(config ?? {})`, preventing `TypeError` during `session/event` event unboxing.
14
+ - **DSH 0.2.0-rc.1 clean activation & volatile schema** (#42): Guarantee clean activation on DSH 0.2.0-rc.1 when `settings.register` is absent; confirm all user-facing fields in `Config` have `.volatile()` markers.
15
+
7
16
  ## [0.4.0] - 2026-10-05
8
17
 
9
18
  ### Added
package/lib/client.js CHANGED
@@ -675,11 +675,10 @@ window.__ModuleLoader__.load({
675
675
  }
676
676
  }
677
677
 
678
- // dsh 0.1 keys the settings scope by package name; 0.2 keys it by the
679
- // composition row id. get() always returns a controller, so hold BOTH and
680
- // use whichever reports ready (the other stays permanently unavailable).
681
- const [scopeNS, scopeRow] = React.useMemo(
682
- () => (ctx && ctx.configForms ? [ctx.configForms.get(NS), ctx.configForms.get(ROW_ID)] : [undefined, undefined]),
678
+ // In DSH 0.2.x configForms is keyed strictly by entry id (ROW_ID = 'plugin-notify').
679
+ // Hold both candidates and prefer ROW_ID whenever it is ready or NS is unavailable.
680
+ const [scopeRow, scopeNS] = React.useMemo(
681
+ () => (ctx && ctx.configForms ? [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)] : [undefined, undefined]),
683
682
  [ctx],
684
683
  )
685
684
  const useSnap = (scope) => React.useSyncExternalStore(
@@ -687,11 +686,11 @@ window.__ModuleLoader__.load({
687
686
  React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
688
687
  React.useCallback(() => ({ status: 'loading' }), []),
689
688
  )
690
- const snapNS = useSnap(scopeNS)
691
689
  const snapRow = useSnap(scopeRow)
692
- const rowWins = scopeRow && snapNS.status !== 'ready' && snapRow.status === 'ready'
693
- const scope = rowWins ? scopeRow : scopeNS
694
- const snapshot = rowWins ? snapRow : snapNS
690
+ const snapNS = useSnap(scopeNS)
691
+ const useRow = scopeRow && (snapRow.status === 'ready' || snapNS.status !== 'ready')
692
+ const scope = useRow ? scopeRow : (scopeNS || scopeRow)
693
+ const snapshot = useRow ? snapRow : (scopeNS ? snapNS : snapRow)
695
694
 
696
695
  React.useEffect(() => { ensureStyles() }, [])
697
696
 
@@ -905,7 +904,7 @@ window.__ModuleLoader__.load({
905
904
  })
906
905
  ctx.slots.inject('settings.plugin.item', () => {
907
906
  ctx.slots.register(
908
- { name: 'settings.plugin.item', key: NS, locale: NS, order: 40, inject: () => ({ ctx }) },
907
+ { name: 'settings.plugin.item', key: ROW_ID, locale: NS, order: 40, inject: () => ({ ctx }) },
909
908
  (props) => React.createElement(NotifyCard, { ...props, ctx }),
910
909
  )
911
910
  })
@@ -916,7 +915,7 @@ window.__ModuleLoader__.load({
916
915
  let currentSettings = {}
917
916
  if (ctx.configForms) {
918
917
  try {
919
- const candidates = [ctx.configForms.get(NS), ctx.configForms.get(ROW_ID)]
918
+ const candidates = [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)]
920
919
  const ready = candidates.find((s) => s && s.getSnapshot && s.getSnapshot().status === 'ready')
921
920
  const scope = ready || candidates[0]
922
921
  if (scope && typeof scope.subscribe === 'function') {
package/lib/index.js CHANGED
@@ -10,6 +10,8 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials';
10
10
  export const name = '@goodandready/dsh-plugin-notify';
11
11
  /** Settings namespace shared with the Web settings card. */
12
12
  export const NS = '@goodandready/dsh-plugin-notify';
13
+ /** Canonical entry id matching cordis.patch.yml */
14
+ export const ENTRY_ID = 'plugin-notify';
13
15
 
14
16
  // Session firehose + credentials (webhook URL refs) + settings scope for the card + webServer for SSE events + connection for authentication.
15
17
  export const inject = ['sessions', 'credentials', 'settings', 'webServer', 'connection'];
@@ -734,7 +736,7 @@ function mountWebServer(targetCtx, hostCtx, getConfig) {
734
736
  }
735
737
 
736
738
  export function apply(ctx, config = {}) {
737
- let getConfig = () => config ?? {};
739
+ let getConfig = () => unwrapConfig(config ?? {});
738
740
 
739
741
  let webServerMounted = false;
740
742
  let unmountWebServer = null;
@@ -768,22 +770,48 @@ export function apply(ctx, config = {}) {
768
770
  if (typeof ctx.inject === 'function') {
769
771
  ctx.inject(['settings'], (sctx) => {
770
772
  if (typeof sctx.settings?.register === 'function') {
771
- const scope = sctx.settings.register(NS, Config, { base: config ?? {} });
772
- getConfig = () => scope.get() ?? config ?? {};
773
- return;
773
+ try {
774
+ const scope = sctx.settings.register(ENTRY_ID, Config, { base: config ?? {} });
775
+ getConfig = () => (scope ? unwrapConfig(scope.get() ?? config ?? {}) : unwrapConfig(config ?? {}));
776
+ return;
777
+ } catch (err) {
778
+ ctx?.logger?.warn?.(`[plugin-notify] settings.register('${ENTRY_ID}') failed, falling back to describe: ${String(err)}`);
779
+ }
780
+ }
781
+ let currentRow = (() => { try { return sctx.settings?.describe?.().find((r) => r && (r.ns === ENTRY_ID || r.ns === NS)); } catch { return null; } })();
782
+ const refreshSettings = () => {
783
+ try {
784
+ const next = sctx.settings?.describe?.().find((r) => r && (r.ns === ENTRY_ID || r.ns === NS));
785
+ if (next) currentRow = next;
786
+ } catch {
787
+ // best effort
788
+ }
789
+ };
790
+ if (typeof ctx.on === 'function') {
791
+ ctx.on('settings/document-updated', refreshSettings);
792
+ ctx.on('loader/volatile-update', refreshSettings);
793
+ ctx.on('settings/updated', refreshSettings);
794
+ }
795
+ if (typeof sctx.settings?.on === 'function') {
796
+ try { sctx.settings.on('update', refreshSettings); } catch { /* ignore */ }
774
797
  }
775
- const row = (() => { try { return sctx.settings?.describe?.().find((r) => r && r.ns === NS); } catch { return null; } })();
776
798
  const scope = {
777
- get: () => row?.value ?? config ?? {},
799
+ get: () => {
800
+ refreshSettings();
801
+ return currentRow?.value ?? config ?? {};
802
+ },
778
803
  replace: async (next) => {
779
804
  try {
780
- return await sctx.settings?.replace?.(NS, next, row?.revision);
805
+ const targetNs = sctx.settings?.describe?.().some((r) => r && r.ns === ENTRY_ID) ? ENTRY_ID : NS;
806
+ const res = await sctx.settings?.replace?.(targetNs, next, currentRow?.revision);
807
+ refreshSettings();
808
+ return res;
781
809
  } catch (err) {
782
810
  ctx?.logger?.warn?.(`[plugin-notify] settings replace failed: ${String(err)}`);
783
811
  }
784
812
  },
785
813
  };
786
- getConfig = () => scope.get() ?? config ?? {};
814
+ getConfig = () => (scope ? unwrapConfig(scope.get() ?? config ?? {}) : unwrapConfig(config ?? {}));
787
815
  });
788
816
  ctx.inject(['webServer'], (wctx) => tryMountWebServer(wctx))
789
817
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-plugin-notify",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "DSH plugin: audio chimes, cross-session toasts, desktop push, IM webhooks, and web push (PWA) for turn completion, errors, and approvals.",
5
5
  "license": "MIT",
6
6
  "type": "module",