@1outres/dsh-plugin-notify 0.0.0-stage → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1outres
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # @1outres/dsh-plugin-notify
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Show a desktop notification when the DeepSeek Harness Web UI is in the
4
+ background and the Session needs you: a tool call is waiting for approval, the
5
+ agent asked a question (or proposed a plan to review), or a turn finished.
6
+
7
+ Install just this plugin:
8
+
9
+ ```sh
10
+ dsh plugin add @1outres/dsh-plugin-notify
11
+ ```
12
+
13
+ Or install it with every plugin in the repository through the
14
+ [`@1outres/dsh-plugins`](../dsh-plugins) bundle. Enable one or the other: both
15
+ insert the same `notify` row.
16
+
17
+ ## Settings
18
+
19
+ Open **Settings → Notifications**.
20
+
21
+ | Field | Default | Meaning |
22
+ | ----------------------------------------- | ------- | ------------------------------------------------------------------------- |
23
+ | Enable notifications | on | Master switch. |
24
+ | Only while this page is in the background | on | While the window is focused and visible, the prompt is on screen already. |
25
+ | Approval prompts | on | A tool call waits for allow or reject. |
26
+ | Questions and plan reviews | on | The agent asked you something, or proposed a plan. |
27
+ | Finished turns | on | A turn stopped running while you were looking elsewhere. |
28
+ | Include request details | on | Adds the tool name, the approval reason, or the question text. |
29
+
30
+ The page also shows the browser permission state, an **Allow notifications**
31
+ button, and **Send test notification**.
32
+
33
+ ## How it works
34
+
35
+ 1. The browser half subscribes to `uiSession.sessionStatus`, the per-Session map
36
+ the shipped UI already maintains (`running`, `pendingInteraction`,
37
+ `completionUnread`).
38
+ 2. A new pending interaction, or a Session that stops running, becomes a Web
39
+ Notification: `Approval needed`, `Question waiting`, `Plan review waiting`,
40
+ or `Turn finished`.
41
+ 3. Notifications are tagged per Session and concern, so a repeated request
42
+ replaces the previous one, and answering the prompt closes it.
43
+ 4. Clicking a notification focuses the window and opens that Session through
44
+ `uiWorkspace.openSession()`.
45
+
46
+ The plugin never registers into the approval waterfall: it only reads state the
47
+ approval UI already publishes, so it cannot drop, delay, or answer a request.
48
+ Notifications are raised by the browser, which means they appear on the machine
49
+ showing the page, not on the Host.
50
+
51
+ ## Limits
52
+
53
+ - No notification while the page is closed: the browser cannot raise one.
54
+ - Notification permission is per browser and per origin; each browser asks once.
55
+ - The Host does not forward tool arguments to the browser, so the body can show
56
+ the tool name and the approval reason but not the full command.
57
+ - Only the highest-precedence pending interaction per Session is visible in the
58
+ status map, so an approval waiting behind a visible question is notified once
59
+ the question is answered.
@@ -0,0 +1,6 @@
1
+ # Installs this plugin on its own (dsh plugin add @1outres/dsh-plugin-notify).
2
+ # The @1outres/dsh-plugins bundle inserts the same "notify" row, so select one
3
+ # bundle or the other, never both.
4
+ - insert:
5
+ - id: notify
6
+ name: "@1outres/dsh-plugin-notify"
package/lib/client.js ADDED
@@ -0,0 +1,635 @@
1
+ /* Generated by scripts/build.mjs — do not edit. */
2
+ window.__ModuleLoader__.load({
3
+ id: "@1outres/dsh-plugin-notify",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // ../dsh-plugin-notify/src/client/index.tsx
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ apply: () => apply,
30
+ inject: () => inject
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // ../dsh-plugin-notify/src/client/config.ts
35
+ var CONFIG_NAMESPACE = "notify";
36
+ var DEFAULT_CONFIG = {
37
+ enabled: true,
38
+ onlyWhenUnfocused: true,
39
+ approval: true,
40
+ question: true,
41
+ completion: true,
42
+ showDetail: true
43
+ };
44
+ function normalizeConfig(value) {
45
+ const read = (field) => {
46
+ const current = value?.[field];
47
+ return typeof current === "boolean" ? current : DEFAULT_CONFIG[field];
48
+ };
49
+ return {
50
+ enabled: read("enabled"),
51
+ onlyWhenUnfocused: read("onlyWhenUnfocused"),
52
+ approval: read("approval"),
53
+ question: read("question"),
54
+ completion: read("completion"),
55
+ showDetail: read("showDetail")
56
+ };
57
+ }
58
+
59
+ // ../dsh-plugin-notify/src/client/i18n.ts
60
+ var en = {
61
+ nav: "Notifications",
62
+ title: "Notifications",
63
+ description: "Raise a desktop notification when this page is in the background: a tool call needs approval, the agent asks a question, or a turn finishes. The notification comes from this browser, on the machine that is showing the page.",
64
+ enabled: "Enable notifications",
65
+ enabledHint: "Turn every notification off without uninstalling the plugin.",
66
+ onlyWhenUnfocused: "Only while this page is in the background",
67
+ onlyWhenUnfocusedHint: "While the window is focused and visible, the prompt is already on screen.",
68
+ approval: "Approval prompts",
69
+ approvalHint: "A tool call is waiting for allow or reject.",
70
+ question: "Questions and plan reviews",
71
+ questionHint: "The agent asked you something, or proposed a plan to review.",
72
+ completion: "Finished turns",
73
+ completionHint: "A turn stopped running while you were looking elsewhere.",
74
+ showDetail: "Include request details",
75
+ showDetailHint: "Adds the tool name, the approval reason, or the question text.",
76
+ permission: "Browser permission",
77
+ permissionGranted: "Allowed",
78
+ permissionDenied: "Blocked. Allow notifications for this site in the browser settings.",
79
+ permissionDefault: "Not requested yet",
80
+ permissionUnsupported: "This browser does not support notifications.",
81
+ allow: "Allow notifications",
82
+ test: "Send test notification",
83
+ testSent: "Test notification sent.",
84
+ testBlocked: "The browser has not allowed notifications.",
85
+ testTitle: "DeepSeek Harness notifications",
86
+ testBody: "Approval and completion alerts will appear like this.",
87
+ loading: "Loading configuration\u2026",
88
+ unavailable: "This configuration is not available.",
89
+ readOnly: "Configuration is read-only.",
90
+ writeError: "The Host refused the change."
91
+ };
92
+
93
+ // ../dsh-plugin-notify/src/client/notify.ts
94
+ var FALLBACK_SESSION = "DeepSeek Harness";
95
+ var MAX_BODY = 180;
96
+ var MAX_DETAIL = 140;
97
+ var TITLES = {
98
+ approval: "Approval needed",
99
+ question: "Question waiting",
100
+ "plan-review": "Plan review waiting",
101
+ completion: "Turn finished"
102
+ };
103
+ function kindOf(pending) {
104
+ switch (pending.kind) {
105
+ case "approval":
106
+ return "approval";
107
+ case "question":
108
+ return "question";
109
+ case "plan-review":
110
+ return "plan-review";
111
+ default:
112
+ return void 0;
113
+ }
114
+ }
115
+ function noticeTag(sessionId, kind) {
116
+ return `dsh-notify:${kind}:${sessionId}`;
117
+ }
118
+ function diffStatus(previous, next, options) {
119
+ const opened = [];
120
+ const closed = [];
121
+ const allowed = (sessionId) => options.notifySession?.(sessionId) ?? true;
122
+ const enabled = (kind) => kind === "approval" ? options.approval : kind === "completion" ? options.completion : options.question;
123
+ for (const [sessionId, status] of next) {
124
+ const before = previous?.get(sessionId);
125
+ const pending = status.pendingInteraction;
126
+ const previousPending = before?.pendingInteraction;
127
+ if (pending !== void 0) {
128
+ const kind = kindOf(pending);
129
+ const replaced = previousPending !== void 0 && pending.key !== previousPending.key;
130
+ if (replaced && previousPending !== void 0) {
131
+ const previousKind = kindOf(previousPending);
132
+ if (previousKind !== void 0 && previousKind !== kind) {
133
+ closed.push(noticeTag(sessionId, previousKind));
134
+ }
135
+ }
136
+ if (kind !== void 0 && pending.key !== previousPending?.key && enabled(kind) && allowed(sessionId)) {
137
+ opened.push({ sessionId, kind, key: pending.key, ...detailOf(pending) });
138
+ }
139
+ } else if (previousPending !== void 0) {
140
+ const kind = kindOf(previousPending);
141
+ if (kind !== void 0) closed.push(noticeTag(sessionId, kind));
142
+ }
143
+ if (before === void 0) continue;
144
+ const finished = before.running === true && status.running === false;
145
+ const becameUnread = before.completionUnread !== true && status.completionUnread === true;
146
+ if ((finished || becameUnread) && options.completion && allowed(sessionId)) {
147
+ opened.push({ sessionId, kind: "completion", key: `completion:${sessionId}` });
148
+ }
149
+ }
150
+ for (const [sessionId, before] of previous ?? []) {
151
+ if (next.has(sessionId)) continue;
152
+ const kind = before.pendingInteraction === void 0 ? void 0 : kindOf(before.pendingInteraction);
153
+ if (kind !== void 0) closed.push(noticeTag(sessionId, kind));
154
+ }
155
+ return { opened, closed };
156
+ }
157
+ function shouldNotify(config, page) {
158
+ if (!config.enabled) return false;
159
+ if (config.onlyWhenUnfocused && page.visible && page.focused) return false;
160
+ return true;
161
+ }
162
+ function formatNotice(event, sessionTitle, options) {
163
+ const session = truncate(sessionTitle, MAX_DETAIL) || FALLBACK_SESSION;
164
+ const detail = options.showDetail ? describe(event) : void 0;
165
+ const body = detail === void 0 ? session : truncate(`${session} \xB7 ${detail}`, MAX_BODY);
166
+ return {
167
+ tag: noticeTag(event.sessionId, event.kind),
168
+ title: TITLES[event.kind],
169
+ body,
170
+ sessionId: event.sessionId
171
+ };
172
+ }
173
+ function truncate(value, max) {
174
+ const text = value.replace(/\s+/g, " ").trim();
175
+ if (text.length <= max) return text;
176
+ return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}\u2026`;
177
+ }
178
+ function describe(event) {
179
+ const reason = event.displayReason?.en ?? event.reason;
180
+ switch (event.kind) {
181
+ case "approval": {
182
+ const parts = [event.toolName, reason].filter(
183
+ (part) => typeof part === "string" && part.trim().length > 0
184
+ );
185
+ return parts.length === 0 ? void 0 : truncate(parts.join(": "), MAX_DETAIL);
186
+ }
187
+ case "question":
188
+ return event.question === void 0 ? void 0 : truncate(event.question, MAX_DETAIL);
189
+ case "plan-review":
190
+ return reason === void 0 ? void 0 : truncate(reason, MAX_DETAIL);
191
+ case "completion":
192
+ return void 0;
193
+ }
194
+ }
195
+ function detailOf(pending) {
196
+ return {
197
+ toolName: pending.toolName,
198
+ reason: pending.reason,
199
+ displayReason: pending.displayReason,
200
+ question: pending.questions?.[0]?.question
201
+ };
202
+ }
203
+
204
+ // ../dsh-plugin-notify/src/client/SettingsPage.tsx
205
+ var import_react = require("react");
206
+ var import_jsx_runtime = require("react/jsx-runtime");
207
+ var PERMISSION_KEYS = {
208
+ granted: "permissionGranted",
209
+ denied: "permissionDenied",
210
+ default: "permissionDefault",
211
+ unsupported: "permissionUnsupported"
212
+ };
213
+ var sectionStyle = {
214
+ maxWidth: 680,
215
+ padding: "4px 0 32px",
216
+ color: "var(--dsw-alias-label-primary)"
217
+ };
218
+ var headingStyle = {
219
+ margin: 0,
220
+ fontSize: 18,
221
+ fontWeight: 600,
222
+ lineHeight: 1.4
223
+ };
224
+ var introStyle = {
225
+ margin: "8px 0 0",
226
+ color: "var(--dsw-alias-label-secondary)",
227
+ fontSize: 13,
228
+ lineHeight: 1.6
229
+ };
230
+ var toggleStyle = {
231
+ display: "flex",
232
+ alignItems: "center",
233
+ justifyContent: "space-between",
234
+ gap: 16,
235
+ marginTop: 20,
236
+ fontSize: 13,
237
+ fontWeight: 600
238
+ };
239
+ var hintStyle = {
240
+ margin: 0,
241
+ color: "var(--dsw-alias-label-tertiary)",
242
+ fontSize: 12,
243
+ lineHeight: 1.5,
244
+ fontWeight: 400
245
+ };
246
+ var errorStyle = {
247
+ margin: "8px 0 0",
248
+ color: "var(--dsw-alias-state-error-primary)",
249
+ fontSize: 12,
250
+ lineHeight: 1.5
251
+ };
252
+ var groupStyle = {
253
+ margin: "26px 0 0",
254
+ paddingTop: 18,
255
+ borderTop: "1px solid var(--dsw-alias-border-l2)"
256
+ };
257
+ var buttonStyle = {
258
+ marginTop: 12,
259
+ marginRight: 8,
260
+ padding: "8px 12px",
261
+ borderRadius: 8,
262
+ border: "1px solid var(--dsw-alias-border-l2)",
263
+ background: "var(--dsw-alias-bg-layer-1)",
264
+ color: "inherit",
265
+ font: "inherit",
266
+ cursor: "pointer"
267
+ };
268
+ var mutedStyle = {
269
+ color: "var(--dsw-alias-label-tertiary)",
270
+ fontSize: 13,
271
+ lineHeight: 1.6
272
+ };
273
+ function SettingsPage({ form, t, notifier }) {
274
+ const subscribe = (0, import_react.useCallback)((listener) => form.subscribe(listener), [form]);
275
+ const getSnapshot = (0, import_react.useCallback)(() => form.getSnapshot(), [form]);
276
+ const snapshot = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
277
+ const config = normalizeConfig(snapshot.value);
278
+ const [permission, setPermission] = (0, import_react.useState)(() => notifier.permission());
279
+ const [busy, setBusy] = (0, import_react.useState)(false);
280
+ const [message, setMessage] = (0, import_react.useState)(void 0);
281
+ const [writeError, setWriteError] = (0, import_react.useState)(false);
282
+ const write = (field, value) => {
283
+ void form.set(field, value).then((accepted) => setWriteError(!accepted)).catch(() => setWriteError(true));
284
+ };
285
+ const allow = async () => {
286
+ setBusy(true);
287
+ setMessage(void 0);
288
+ try {
289
+ setPermission(await notifier.request());
290
+ } finally {
291
+ setBusy(false);
292
+ }
293
+ };
294
+ const test = async () => {
295
+ setBusy(true);
296
+ setMessage(void 0);
297
+ try {
298
+ const shown = await notifier.test();
299
+ setPermission(notifier.permission());
300
+ setMessage(shown ? t("testSent") : t("testBlocked"));
301
+ } finally {
302
+ setBusy(false);
303
+ }
304
+ };
305
+ if (snapshot.status === "unavailable") {
306
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: mutedStyle, children: t("unavailable") });
307
+ }
308
+ const disabled = snapshot.writable === false || busy;
309
+ const unsupported = permission === "unsupported";
310
+ const permissionLabel = t(PERMISSION_KEYS[permission] ?? "permissionDefault");
311
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: sectionStyle, children: [
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: headingStyle, children: t("title") }),
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: introStyle, children: t("description") }),
314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
315
+ Toggle,
316
+ {
317
+ label: t("enabled"),
318
+ hint: t("enabledHint"),
319
+ checked: config.enabled,
320
+ disabled,
321
+ onChange: (next) => write("enabled", next)
322
+ }
323
+ ),
324
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
325
+ Toggle,
326
+ {
327
+ label: t("onlyWhenUnfocused"),
328
+ hint: t("onlyWhenUnfocusedHint"),
329
+ checked: config.onlyWhenUnfocused,
330
+ disabled,
331
+ onChange: (next) => write("onlyWhenUnfocused", next)
332
+ }
333
+ ),
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: groupStyle, children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
336
+ Toggle,
337
+ {
338
+ label: t("approval"),
339
+ hint: t("approvalHint"),
340
+ checked: config.approval,
341
+ disabled,
342
+ onChange: (next) => write("approval", next)
343
+ }
344
+ ),
345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
+ Toggle,
347
+ {
348
+ label: t("question"),
349
+ hint: t("questionHint"),
350
+ checked: config.question,
351
+ disabled,
352
+ onChange: (next) => write("question", next)
353
+ }
354
+ ),
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
356
+ Toggle,
357
+ {
358
+ label: t("completion"),
359
+ hint: t("completionHint"),
360
+ checked: config.completion,
361
+ disabled,
362
+ onChange: (next) => write("completion", next)
363
+ }
364
+ ),
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
366
+ Toggle,
367
+ {
368
+ label: t("showDetail"),
369
+ hint: t("showDetailHint"),
370
+ checked: config.showDetail,
371
+ disabled,
372
+ onChange: (next) => write("showDetail", next)
373
+ }
374
+ )
375
+ ] }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: groupStyle, children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toggleStyle, children: [
378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("permission") }),
379
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: 400, color: "var(--dsw-alias-label-secondary)" }, children: permissionLabel })
380
+ ] }),
381
+ permission === "default" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
382
+ "button",
383
+ {
384
+ type: "button",
385
+ style: buttonStyle,
386
+ disabled,
387
+ onClick: () => void allow(),
388
+ children: t("allow")
389
+ }
390
+ ) : null,
391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
392
+ "button",
393
+ {
394
+ type: "button",
395
+ style: buttonStyle,
396
+ disabled: disabled || unsupported,
397
+ onClick: () => void test(),
398
+ children: t("test")
399
+ }
400
+ ),
401
+ message === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: hintStyle, children: message }),
402
+ writeError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: errorStyle, children: t("writeError") }) : null
403
+ ] })
404
+ ] });
405
+ }
406
+ function Toggle({ label, hint, checked, disabled, onChange }) {
407
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: toggleStyle, children: [
408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label }),
410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: hintStyle, children: hint })
411
+ ] }),
412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ role: "switch",
417
+ "aria-checked": checked,
418
+ "aria-label": label,
419
+ className: "dsh-notify-switch",
420
+ disabled,
421
+ onClick: (event) => {
422
+ event.stopPropagation();
423
+ onChange(!checked);
424
+ },
425
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-notify-thumb" })
426
+ }
427
+ )
428
+ ] });
429
+ }
430
+
431
+ // ../dsh-plugin-notify/src/client/styles.ts
432
+ var NOTIFY_CSS = `
433
+ .dsh-notify-switch {
434
+ box-sizing: border-box;
435
+ position: relative;
436
+ flex: 0 0 auto;
437
+ width: 36px;
438
+ height: 20px;
439
+ padding: 2px;
440
+ border: 0;
441
+ border-radius: 999px;
442
+ background: var(--dsw-alias-border-l3);
443
+ cursor: pointer;
444
+ }
445
+
446
+ .dsh-notify-switch[aria-checked="true"] {
447
+ background: var(--dsw-alias-brand-primary);
448
+ }
449
+
450
+ .dsh-notify-switch:disabled {
451
+ cursor: default;
452
+ opacity: 0.5;
453
+ }
454
+
455
+ .dsh-notify-switch:focus-visible {
456
+ outline: var(--dsw-focus-ring-width) solid
457
+ var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
458
+ outline-offset: 2px;
459
+ }
460
+
461
+ .dsh-notify-thumb {
462
+ display: block;
463
+ width: 16px;
464
+ height: 16px;
465
+ border-radius: 50%;
466
+ background: var(--dsw-alias-label-primary-foreground);
467
+ transition: transform 0.12s ease;
468
+ }
469
+
470
+ .dsh-notify-switch[aria-checked="false"] .dsh-notify-thumb {
471
+ background: var(--dsw-alias-switch-thumb);
472
+ }
473
+
474
+ .dsh-notify-switch[aria-checked="true"] .dsh-notify-thumb {
475
+ transform: translate(16px);
476
+ }
477
+ `;
478
+
479
+ // ../dsh-plugin-notify/src/client/index.tsx
480
+ var import_jsx_runtime2 = require("react/jsx-runtime");
481
+ var inject = ["sessions", "uiSession", "uiWorkspace", "locale", "configForms", "slots"];
482
+ var PLUGIN_ID = "@1outres/dsh-plugin-notify";
483
+ var TEST_TAG = `${PLUGIN_ID}:test`;
484
+ function apply(ctx) {
485
+ const t = ctx.locale.bind(CONFIG_NAMESPACE);
486
+ ctx.effect(() => ctx.locale.register(CONFIG_NAMESPACE, { en }), "notify: dictionaries");
487
+ ctx.effect(() => {
488
+ const tag = document.createElement("style");
489
+ tag.dataset.plugin = PLUGIN_ID;
490
+ tag.dataset.pluginCss = `${PLUGIN_ID}/notify.css`;
491
+ tag.textContent = NOTIFY_CSS;
492
+ document.head.append(tag);
493
+ return () => {
494
+ tag.remove();
495
+ };
496
+ }, "notify: stylesheet");
497
+ const form = ctx.configForms.get(CONFIG_NAMESPACE);
498
+ let config = normalizeConfig(form.getSnapshot().value);
499
+ ctx.effect(() => {
500
+ const read = () => {
501
+ config = normalizeConfig(form.getSnapshot().value);
502
+ };
503
+ const off = form.subscribe(read);
504
+ read();
505
+ return off;
506
+ }, "notify: configuration");
507
+ const live = /* @__PURE__ */ new Map();
508
+ let autoRequested = false;
509
+ const supported = () => typeof Notification !== "undefined";
510
+ const permission = () => supported() ? Notification.permission : "unsupported";
511
+ const close = (tag) => {
512
+ const notification = live.get(tag);
513
+ if (notification === void 0) return;
514
+ live.delete(tag);
515
+ try {
516
+ notification.close();
517
+ } catch {
518
+ }
519
+ };
520
+ const closeAll = () => {
521
+ for (const tag of [...live.keys()]) close(tag);
522
+ };
523
+ const request = async () => {
524
+ if (!supported()) return "unsupported";
525
+ if (Notification.permission !== "default") return Notification.permission;
526
+ try {
527
+ return await Notification.requestPermission();
528
+ } catch {
529
+ return Notification.permission;
530
+ }
531
+ };
532
+ const show = (spec) => {
533
+ if (!supported()) return;
534
+ close(spec.tag);
535
+ const notification = new Notification(spec.title, { body: spec.body, tag: spec.tag });
536
+ live.set(spec.tag, notification);
537
+ notification.onclick = () => {
538
+ try {
539
+ if (spec.sessionId.length > 0) ctx.uiWorkspace.openSession(spec.sessionId);
540
+ } catch {
541
+ }
542
+ try {
543
+ window.focus();
544
+ } catch {
545
+ }
546
+ close(spec.tag);
547
+ };
548
+ notification.onclose = () => {
549
+ if (live.get(spec.tag) === notification) live.delete(spec.tag);
550
+ };
551
+ };
552
+ const push = async (spec) => {
553
+ if (!supported()) return;
554
+ let state = permission();
555
+ if (state === "default" && !autoRequested) {
556
+ autoRequested = true;
557
+ state = await request();
558
+ }
559
+ if (state !== "granted") return;
560
+ show(spec);
561
+ };
562
+ const pageState = () => ({
563
+ visible: typeof document === "undefined" ? true : document.visibilityState !== "hidden",
564
+ focused: typeof document === "undefined" ? true : document.hasFocus()
565
+ });
566
+ const rowOf = (sessionId) => ctx.sessions.list.getSnapshot().byId[sessionId];
567
+ const isListed = (sessionId) => {
568
+ const row = rowOf(sessionId);
569
+ return row !== void 0 && row.origin !== "subagent";
570
+ };
571
+ const titleOf = (sessionId) => {
572
+ const row = rowOf(sessionId);
573
+ return row?.title ?? row?.displayTitle ?? "";
574
+ };
575
+ const status = ctx.uiSession.sessionStatus;
576
+ let previous;
577
+ const sync = () => {
578
+ const next = status.getSnapshot();
579
+ const diff = diffStatus(previous, next, {
580
+ approval: config.approval,
581
+ question: config.question,
582
+ completion: config.completion,
583
+ notifySession: isListed
584
+ });
585
+ previous = next;
586
+ for (const tag of diff.closed) close(tag);
587
+ if (!shouldNotify(config, pageState())) return;
588
+ for (const event of diff.opened) {
589
+ void push(formatNotice(event, titleOf(event.sessionId), { showDetail: config.showDetail }));
590
+ }
591
+ };
592
+ ctx.effect(() => {
593
+ const off = status.subscribe(sync);
594
+ sync();
595
+ return () => {
596
+ off();
597
+ closeAll();
598
+ };
599
+ }, "notify: session status");
600
+ const notifier = {
601
+ permission,
602
+ request,
603
+ test: async () => {
604
+ const state = await request();
605
+ if (state !== "granted") return false;
606
+ show({ tag: TEST_TAG, title: t("testTitle"), body: t("testBody"), sessionId: "" });
607
+ return true;
608
+ }
609
+ };
610
+ const translate = t;
611
+ const Page = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SettingsPage, { form, t: translate, notifier });
612
+ ctx.effect(
613
+ () => ctx.configForms.whileServed(
614
+ [CONFIG_NAMESPACE],
615
+ () => ctx.slots.inject(
616
+ "settings.section",
617
+ () => ctx.slots.register(
618
+ {
619
+ name: "settings.section",
620
+ id: "notify",
621
+ order: 61,
622
+ label: () => t("nav"),
623
+ locale: CONFIG_NAMESPACE
624
+ },
625
+ Page
626
+ )
627
+ )
628
+ ),
629
+ "notify: settings section"
630
+ );
631
+ }
632
+
633
+ return module.exports;
634
+ }
635
+ });
package/lib/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // ../dsh-plugin-notify/src/index.ts
2
+ import z from "@deepseek-ai/schemastery";
3
+ var Config = z.object({
4
+ enabled: z.boolean().default(true).volatile(),
5
+ onlyWhenUnfocused: z.boolean().default(true).volatile(),
6
+ approval: z.boolean().default(true).volatile(),
7
+ question: z.boolean().default(true).volatile(),
8
+ completion: z.boolean().default(true).volatile(),
9
+ showDetail: z.boolean().default(true).volatile()
10
+ });
11
+ function apply(ctx) {
12
+ ctx.inject(["settings"], (child) => {
13
+ const settings = child.settings;
14
+ child.effect(
15
+ () => settings.configure({ auto: false }, ctx.fiber),
16
+ "notify: settings page policy"
17
+ );
18
+ });
19
+ }
20
+ export {
21
+ Config,
22
+ apply
23
+ };
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@1outres/dsh-plugin-notify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Desktop notifications for approval prompts, agent questions, and finished turns in the DeepSeek Harness Web UI.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/1outres/my-dsh-plugins.git",
8
+ "directory": "packages/dsh-plugin-notify"
9
+ },
10
+ "type": "module",
11
+ "main": "lib/index.js",
12
+ "exports": {
13
+ ".": "./lib/index.js",
14
+ "./client": "./lib/client.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml"
20
+ ],
21
+ "keywords": [
22
+ "dsh",
23
+ "dsh-plugin",
24
+ "notifications",
25
+ "desktop-notifications",
26
+ "deepseek-harness"
27
+ ],
28
+ "license": "MIT",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "dsh": ">=0.2.1-rc.5 <0.3.0",
34
+ "node": ">=22"
35
+ },
36
+ "dependencies": {
37
+ "@deepseek-ai/schemastery": "~3.18.5"
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "~4.0.5",
41
+ "react": "^18.2.0"
42
+ },
43
+ "dsh": {
44
+ "bundle": {
45
+ "patch": [
46
+ "./cordis.patch.yml"
47
+ ]
48
+ },
49
+ "client": {
50
+ "platform": "web",
51
+ "inject": [
52
+ "@deepseek-ai/dsh-client-ui-renderer",
53
+ "@deepseek-ai/dsh-client-ui-settings",
54
+ "@deepseek-ai/dsh-client-ui-session",
55
+ "@deepseek-ai/dsh-client-ui-workspace",
56
+ "@deepseek-ai/dsh-api-session-controller"
57
+ ]
58
+ }
59
+ }
6
60
  }