@1outres/dsh-plugin-notify 0.0.0-stage → 0.3.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 +21 -0
- package/README.md +58 -2
- package/cordis.patch.yml +6 -0
- package/lib/client.js +635 -0
- package/lib/index.js +23 -0
- package/package.json +57 -3
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
|
-
#
|
|
1
|
+
# @1outres/dsh-plugin-notify
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/cordis.patch.yml
ADDED
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.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
|
}
|