@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 +13 -0
- package/lib/client.js +57 -24
- package/lib/index.js +89 -7
- package/package.json +5 -5
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
|
-
|
|
264
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
679
|
-
//
|
|
680
|
-
const
|
|
681
|
-
() => (ctx && ctx.configForms ?
|
|
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
|
|
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
|
-
|
|
752
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
+
"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.
|
|
50
|
-
"@deepseek-ai/dsh-credentials": "^0.1.
|
|
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
|
},
|