@goodandready/dsh-plugin-notify 0.4.3 → 0.4.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
@@ -4,6 +4,19 @@ 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.5] - 2026-10-19
8
+
9
+ ### Fixed
10
+ - **Detached Logger Binding** (#62, GitHub #3): Guard logger method invocations with `safeLog(ctx, message)` binding receiver `this` (`fn.call(logger, message)`) to prevent `TypeError: this is not a function` in Cordis 0.2 `LoggerService`.
11
+ - **Object Settings Persistence** (#62, GitHub #3): Decompose non-leaf object configuration fields (`webhooks`, `dnd`, `webPush`) into individual leaf paths (`['webhooks', key]`) and save atomically via `scope.mutate(ops)` with fallback to `scope.set`, passing `dsh-settings` volatile path validation.
12
+ - **Session Events & Title Cascade** (#62, GitHub #3): Add `eventsOf(session)` supporting append-only `session.log`, `session.eventsSnapshot`, and `session.snapshotEvents()`; add `cachedTitle(session)` reading projected title cache from disk (`storages/session_projcache/sessions/<id>.json`), preventing fallback to raw UUIDs.
13
+ - **Session Navigation Priority** (#62, GitHub #3): Target `uiWorkspace.openSession(sessionId)` as the primary navigation service in `navigateToSession` with graceful fallbacks.
14
+
15
+ ## [0.4.4] - 2026-10-15
16
+
17
+ ### Fixed
18
+ - **Client config form lookup** (#61): Query `ctx.configForms` strictly by canonical entry id `plugin-notify` (`ROW_ID`) rather than attempting fallback queries to scoped package name `@goodandready/dsh-plugin-notify`, fixing unresolvable configuration lookups in the settings panel.
19
+
7
20
  ## [0.4.3] - 2026-10-15
8
21
 
9
22
  ### Fixed
package/lib/client.js CHANGED
@@ -260,15 +260,33 @@ window.__ModuleLoader__.load({
260
260
  function navigateToSession(ctx, sessionId) {
261
261
  if (!sessionId) return
262
262
  try {
263
- if (ctx && ctx.sessions && typeof ctx.sessions.activate === 'function') {
264
- ctx.sessions.activate(sessionId)
263
+ const ws = (ctx && typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : null) || ctx?.uiWorkspace
264
+ if (ws && typeof ws.openSession === 'function') {
265
+ ws.openSession(sessionId)
265
266
  return
266
267
  }
267
268
  } catch (_err) {
268
- // best effort: session navigation fallback
269
+ // best effort: uiWorkspace navigation fallback
270
+ }
271
+ try {
272
+ const s = (ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : null) || ctx?.sessions
273
+ if (s && typeof s.activate === 'function') {
274
+ s.activate(sessionId)
275
+ return
276
+ }
277
+ if (s && typeof s.open === 'function') {
278
+ s.open(sessionId)
279
+ return
280
+ }
281
+ } catch (_err) {
282
+ // best effort: session service fallback
269
283
  }
270
284
  if (typeof window !== 'undefined' && window.location) {
271
- window.location.hash = `session-${sessionId}`
285
+ try {
286
+ window.location.hash = `session-${sessionId}`
287
+ } catch (_err) {
288
+ // best effort: location hash update fallback
289
+ }
272
290
  }
273
291
  }
274
292
 
@@ -371,11 +389,9 @@ window.__ModuleLoader__.load({
371
389
  const timer = setInterval(() => {
372
390
  tries++
373
391
  try {
374
- if (ctx && ctx.sessions && typeof ctx.sessions.activate === 'function') {
375
- ctx.sessions.activate(sessionId)
376
- clearInterval(timer)
377
- return
378
- }
392
+ navigateToSession(ctx, sessionId)
393
+ clearInterval(timer)
394
+ return
379
395
  } catch (_err) {
380
396
  // retry until the budget is exhausted
381
397
  }
@@ -675,22 +691,17 @@ window.__ModuleLoader__.load({
675
691
  }
676
692
  }
677
693
 
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]),
694
+ // In DSH configForms is keyed by entry id (ROW_ID = 'plugin-notify').
695
+ // Query ROW_ID directly; no entry id in the environment has the package name.
696
+ const scope = React.useMemo(
697
+ () => (ctx && ctx.configForms ? ctx.configForms.get(ROW_ID) : undefined),
682
698
  [ctx],
683
699
  )
684
- const useSnap = (scope) => React.useSyncExternalStore(
700
+ const snapshot = React.useSyncExternalStore(
685
701
  React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
686
702
  React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
687
703
  React.useCallback(() => ({ status: 'loading' }), []),
688
704
  )
689
- const snapRow = useSnap(scopeRow)
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)
694
705
 
695
706
  React.useEffect(() => { ensureStyles() }, [])
696
707
 
@@ -747,9 +758,33 @@ window.__ModuleLoader__.load({
747
758
  includeDuration: !!draft.includeDuration,
748
759
  excludeSessionPrefixes: splitList(draft.excludePrefixes),
749
760
  }
761
+ const ops = []
750
762
  for (const [k, v] of Object.entries(payload)) {
751
- try { await scope.set(k, v) }
752
- catch (e) { broken.push(k + ': ' + (e && e.message || String(e))) }
763
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
764
+ for (const [subKey, subVal] of Object.entries(v)) {
765
+ ops.push({ op: 'set', path: [k, subKey], value: subVal })
766
+ }
767
+ } else {
768
+ ops.push({ op: 'set', path: [k], value: v })
769
+ }
770
+ }
771
+
772
+ if (scope && typeof scope.mutate === 'function') {
773
+ try {
774
+ const ok = await scope.mutate(ops)
775
+ if (ok === false) broken.push('mutate refused by host')
776
+ } catch (e) {
777
+ broken.push(e && e.message || String(e))
778
+ }
779
+ } else if (scope && typeof scope.set === 'function') {
780
+ for (const op of ops) {
781
+ try {
782
+ if (op.path.length === 1) await scope.set(op.path[0], op.value)
783
+ else await scope.set(op.path.join('.'), op.value)
784
+ } catch (e) {
785
+ broken.push(op.path.join('.') + ': ' + (e && e.message || String(e)))
786
+ }
787
+ }
753
788
  }
754
789
  setSaving(false)
755
790
  if (broken.length) { setErr(t('settings.saveFailed') + broken.join('; ')); return }
@@ -909,9 +944,7 @@ window.__ModuleLoader__.load({
909
944
  let currentSettings = {}
910
945
  if (ctx.configForms) {
911
946
  try {
912
- const candidates = [ctx.configForms.get(ROW_ID), ctx.configForms.get(NS)]
913
- const ready = candidates.find((s) => s && s.getSnapshot && s.getSnapshot().status === 'ready')
914
- const scope = ready || candidates[0]
947
+ const scope = ctx.configForms.get(ROW_ID)
915
948
  if (scope && typeof scope.subscribe === 'function') {
916
949
  scope.subscribe(() => {
917
950
  const snap = scope.getSnapshot ? scope.getSnapshot() : null
package/lib/index.js CHANGED
@@ -741,6 +741,18 @@ function mountWebServer(targetCtx, hostCtx, getConfig) {
741
741
  return cleanup;
742
742
  }
743
743
 
744
+ export function safeLog(ctx, message) {
745
+ try {
746
+ const logger = ctx?.logger;
747
+ const fn = logger?.debug ?? logger?.info;
748
+ if (typeof fn === 'function') {
749
+ fn.call(logger, message);
750
+ }
751
+ } catch {
752
+ // best effort: logger call must never throw or disrupt execution
753
+ }
754
+ }
755
+
744
756
  export function apply(ctx, config = {}) {
745
757
  let getConfig = () => unwrapConfig(config ?? {});
746
758
 
@@ -827,7 +839,7 @@ export function apply(ctx, config = {}) {
827
839
  const cfg = unwrapConfig(getConfig());
828
840
  const dnd = cfg.dnd;
829
841
  if (inDnd(dnd)) {
830
- (ctx?.logger?.debug ?? ctx?.logger?.info)?.(`[plugin-notify] ${n.kind} · ${n.title} · session ${n.sessionId} · DND (${dnd?.start}-${dnd?.end}), logged only`);
842
+ safeLog(ctx, `[plugin-notify] ${n.kind} · ${n.title} · session ${n.sessionId} · DND (${dnd?.start}-${dnd?.end}), logged only`);
831
843
  return;
832
844
  }
833
845
  const timeoutMs = cfg.timeoutMs ?? 5000;
@@ -863,7 +875,7 @@ export function apply(ctx, config = {}) {
863
875
  ctx?.logger?.warn?.(`[plugin-notify] web push dispatch failed: ${String(error)}`);
864
876
  });
865
877
  } else {
866
- (ctx?.logger?.debug ?? ctx?.logger?.info)?.(`[plugin-notify] ${n.kind} · session ${n.sessionId} · web push skipped (clients connected)`);
878
+ safeLog(ctx, `[plugin-notify] ${n.kind} · session ${n.sessionId} · web push skipped (clients connected)`);
867
879
  }
868
880
  }
869
881
  };
@@ -926,7 +938,7 @@ function send(ctx, n, webhooks, timeoutMs, local, includeSession, includeDuratio
926
938
  const text = renderText(n, includeSession, includeDuration);
927
939
  const signal = AbortSignal.timeout(timeoutMs);
928
940
  const channels = Object.keys(webhooks);
929
- (ctx?.logger?.debug ?? ctx?.logger?.info)?.(`[plugin-notify] ${n.kind} · ${n.title} · session ${n.sessionId} · channels ${channels.join(',') || 'none'} · local ${local}`);
941
+ safeLog(ctx, `[plugin-notify] ${n.kind} · ${n.title} · session ${n.sessionId} · channels ${channels.join(',') || 'none'} · local ${local}`);
930
942
  const post = (channel, url, body) => {
931
943
  fetch(url, {
932
944
  method: 'POST',
@@ -1034,20 +1046,90 @@ export function textOf(content) {
1034
1046
  return out;
1035
1047
  }
1036
1048
 
1049
+ export function eventsOf(session) {
1050
+ const pick = (v) => Array.isArray(v) ? v : (v && Array.isArray(v.entries) ? v.entries : null);
1051
+ try {
1052
+ for (const key of ['events', 'log', 'eventsSnapshot', 'history']) {
1053
+ const raw = session?.[key];
1054
+ const direct = pick(raw);
1055
+ if (direct) return direct;
1056
+ if (typeof raw === 'function') {
1057
+ const got = pick(raw.call(session));
1058
+ if (got) return got;
1059
+ }
1060
+ }
1061
+ if (typeof session?.snapshotEvents === 'function') {
1062
+ const got = pick(session.snapshotEvents());
1063
+ if (got) return got;
1064
+ }
1065
+ } catch (_err) {
1066
+ // best effort: session events resolution fallback
1067
+ }
1068
+ return [];
1069
+ }
1070
+
1071
+ export function cachedTitle(session) {
1072
+ const id = String(session?.id ?? '');
1073
+ if (!id) return '';
1074
+ try {
1075
+ const home = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh');
1076
+ const projPath = path.join(home, 'storages', 'session_projcache', 'sessions', `${id}.json`);
1077
+ if (fs.existsSync(projPath)) {
1078
+ const content = fs.readFileSync(projPath, 'utf8');
1079
+ const doc = JSON.parse(content);
1080
+ const raw = doc?.record?.rows?.title ?? doc?.rows?.title;
1081
+ const val = typeof raw === 'string' ? raw : (raw && typeof raw.val === 'string' ? raw.val : '');
1082
+ if (val && typeof val === 'string') return val.trim();
1083
+ }
1084
+ } catch (_err) {
1085
+ // best effort: session projcache file read fallback
1086
+ }
1087
+ return '';
1088
+ }
1089
+
1037
1090
  export function sessionTitle(session) {
1038
- const events = Array.isArray(session?.events) ? session.events : [];
1091
+ if (typeof session?.title === 'string' && session.title.trim()) {
1092
+ return session.title.trim();
1093
+ }
1094
+ const headerTitle = session?.header?.title;
1095
+ if (typeof headerTitle === 'string' && headerTitle.trim()) {
1096
+ return headerTitle.trim();
1097
+ }
1098
+ if (headerTitle && typeof headerTitle.val === 'string' && headerTitle.val.trim()) {
1099
+ return headerTitle.val.trim();
1100
+ }
1101
+ const rowsTitle = session?.rows?.title;
1102
+ if (typeof rowsTitle === 'string' && rowsTitle.trim()) {
1103
+ return rowsTitle.trim();
1104
+ }
1105
+ if (rowsTitle && typeof rowsTitle.val === 'string' && rowsTitle.val.trim()) {
1106
+ return rowsTitle.val.trim();
1107
+ }
1108
+
1109
+ const events = eventsOf(session);
1110
+ for (const event of events) {
1111
+ if (event && event.type === 'session/title' && event.data?.title) {
1112
+ const t = String(event.data.title).replace(/\s+/g, ' ').trim();
1113
+ if (t) return t.length > 60 ? `${t.slice(0, 60)}…` : t;
1114
+ }
1115
+ }
1039
1116
  for (const event of events) {
1040
1117
  if (event && event.type === 'user/message') {
1041
1118
  const text = textOf(event.data?.content).replace(/\s+/g, ' ').trim();
1042
- if (text)
1043
- return text.length > 60 ? `${text.slice(0, 60)}…` : text;
1119
+ if (text) return text.length > 60 ? `${text.slice(0, 60)}…` : text;
1044
1120
  }
1045
1121
  }
1122
+
1123
+ const cached = cachedTitle(session);
1124
+ if (cached) {
1125
+ return cached.length > 60 ? `${cached.slice(0, 60)}…` : cached;
1126
+ }
1127
+
1046
1128
  return String(session?.id ?? 'session');
1047
1129
  }
1048
1130
 
1049
1131
  export function summarizeTurn(session, turn) {
1050
- const events = Array.isArray(session?.events) ? session.events : [];
1132
+ const events = eventsOf(session);
1051
1133
  let toolCalls = 0;
1052
1134
  let lastText = '';
1053
1135
  // Reverse iteration: events for current turn are at the end of the events array
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-plugin-notify",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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",
@@ -40,14 +40,14 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
- "@deepseek-ai/dsh-session": "^0.1.7-rc.2 || ^0.2.0-rc.1",
44
- "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
43
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
44
+ "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
45
45
  "@deepseek-ai/schemastery": "^3.18.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@deepseek-ai/cordis": "^4.0.1",
49
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
50
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
49
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
51
51
  "@deepseek-ai/schemastery": "^3.18.4",
52
52
  "web-push": "^3.6.7"
53
53
  },