@serkanalgur/opencodev2-notification 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -14
- package/package.json +9 -2
- package/src/index.ts +17 -54
package/README.md
CHANGED
|
@@ -51,10 +51,9 @@ Or copy the plugin files to your `.opencode/plugins/` directory:
|
|
|
51
51
|
|
|
52
52
|
| Event | Notifies? | Sound | Why |
|
|
53
53
|
|-------|-----------|-------|-----|
|
|
54
|
-
| Session complete | Yes |
|
|
55
|
-
| Session error | Yes |
|
|
56
|
-
| Permission needed | Yes |
|
|
57
|
-
| Question asked | Yes | Submarine (default) | Questions should always reach you promptly |
|
|
54
|
+
| Session complete | Yes | done | Main task done - time to review |
|
|
55
|
+
| Session error | Yes | error | Something broke - needs attention |
|
|
56
|
+
| Permission needed | Yes | permission | AI is blocked, waiting for you |
|
|
58
57
|
| Sub-task complete/error | No (default) | - | Set `notifyChildSessions: true` to include child sessions |
|
|
59
58
|
|
|
60
59
|
The plugin automatically:
|
|
@@ -79,10 +78,9 @@ Works out of the box. To customize, create `~/.config/opencode/opencodev2-notifi
|
|
|
79
78
|
{
|
|
80
79
|
"notifyChildSessions": false,
|
|
81
80
|
"sounds": {
|
|
82
|
-
"idle": "
|
|
83
|
-
"error": "
|
|
84
|
-
"permission": "
|
|
85
|
-
"question": "Submarine"
|
|
81
|
+
"idle": "done",
|
|
82
|
+
"error": "error",
|
|
83
|
+
"permission": "permission"
|
|
86
84
|
},
|
|
87
85
|
"quietHours": {
|
|
88
86
|
"enabled": false,
|
|
@@ -97,17 +95,16 @@ Works out of the box. To customize, create `~/.config/opencode/opencodev2-notifi
|
|
|
97
95
|
| Key | Type | Default | Description |
|
|
98
96
|
|-----|------|---------|-------------|
|
|
99
97
|
| `notifyChildSessions` | boolean | `false` | Include child/sub-session notifications |
|
|
100
|
-
| `sounds.idle` | string | `"
|
|
101
|
-
| `sounds.error` | string | `"
|
|
102
|
-
| `sounds.permission` | string | `"
|
|
103
|
-
| `sounds.question` | string | `"Submarine"` | Sound for questions |
|
|
98
|
+
| `sounds.idle` | string | `"done"` | Sound for session complete |
|
|
99
|
+
| `sounds.error` | string | `"error"` | Sound for errors |
|
|
100
|
+
| `sounds.permission` | string | `"permission"` | Sound for permission requests |
|
|
104
101
|
| `quietHours.enabled` | boolean | `false` | Enable quiet hours |
|
|
105
102
|
| `quietHours.start` | string | `"22:00"` | Quiet hours start (HH:MM) |
|
|
106
103
|
| `quietHours.end` | string | `"08:00"` | Quiet hours end (HH:MM) |
|
|
107
104
|
|
|
108
|
-
### Available
|
|
105
|
+
### Available Sound Names
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
`default`, `question`, `permission`, `error`, `done`, `subagent_done`
|
|
111
108
|
|
|
112
109
|
## FAQ
|
|
113
110
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serkanalgur/opencodev2-notification",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Native OS notifications for OpenCode V2 - know when tasks complete, errors occur, or the AI needs your input",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -42,10 +42,17 @@
|
|
|
42
42
|
"@opencode/plugin": "^2.0.0",
|
|
43
43
|
"@types/bun": "latest",
|
|
44
44
|
"prettier": "^3.4.0",
|
|
45
|
+
"solid-js": ">=1.9.0",
|
|
45
46
|
"tsx": "^4.23.15",
|
|
46
47
|
"typescript": "^5.7.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependencies": {
|
|
49
|
-
"@opencode/plugin": ">=2.0.0"
|
|
50
|
+
"@opencode/plugin": ">=2.0.0",
|
|
51
|
+
"solid-js": ">=1.9.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"solid-js": {
|
|
55
|
+
"optional": false
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,15 +22,16 @@ import { Plugin } from "@opencode/plugin/tui"
|
|
|
22
22
|
// TYPES
|
|
23
23
|
// ==========================================
|
|
24
24
|
|
|
25
|
+
type AttentionSoundName = "default" | "question" | "permission" | "error" | "done" | "subagent_done"
|
|
26
|
+
|
|
25
27
|
interface NotifyConfig {
|
|
26
28
|
/** Notify for child/sub-session events (default: false) */
|
|
27
29
|
notifyChildSessions: boolean
|
|
28
30
|
/** Sound configuration per event type */
|
|
29
31
|
sounds: {
|
|
30
|
-
idle:
|
|
31
|
-
error:
|
|
32
|
-
permission:
|
|
33
|
-
question?: string
|
|
32
|
+
idle: AttentionSoundName
|
|
33
|
+
error: AttentionSoundName
|
|
34
|
+
permission: AttentionSoundName
|
|
34
35
|
}
|
|
35
36
|
/** Quiet hours configuration */
|
|
36
37
|
quietHours: {
|
|
@@ -38,8 +39,6 @@ interface NotifyConfig {
|
|
|
38
39
|
start: string // "HH:MM" format
|
|
39
40
|
end: string // "HH:MM" format
|
|
40
41
|
}
|
|
41
|
-
/** Override terminal detection (optional) */
|
|
42
|
-
terminal?: string
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
// ==========================================
|
|
@@ -49,9 +48,9 @@ interface NotifyConfig {
|
|
|
49
48
|
const DEFAULT_CONFIG: NotifyConfig = {
|
|
50
49
|
notifyChildSessions: false,
|
|
51
50
|
sounds: {
|
|
52
|
-
idle: "
|
|
53
|
-
error: "
|
|
54
|
-
permission: "
|
|
51
|
+
idle: "done",
|
|
52
|
+
error: "error",
|
|
53
|
+
permission: "permission",
|
|
55
54
|
},
|
|
56
55
|
quietHours: {
|
|
57
56
|
enabled: false,
|
|
@@ -125,7 +124,6 @@ function isQuietHours(config: NotifyConfig): boolean {
|
|
|
125
124
|
|
|
126
125
|
type RecentNotifications = Map<string, number>
|
|
127
126
|
|
|
128
|
-
const QUESTION_DEDUPE_WINDOW_MS = 1500
|
|
129
127
|
const READY_DEDUPE_WINDOW_MS = 1500
|
|
130
128
|
const PERMISSION_DEDUPE_WINDOW_MS = 1500
|
|
131
129
|
|
|
@@ -169,7 +167,6 @@ export default Plugin.define({
|
|
|
169
167
|
const config = await loadConfig()
|
|
170
168
|
|
|
171
169
|
// Deduplication maps
|
|
172
|
-
const recentQuestionNotifications: RecentNotifications = new Map()
|
|
173
170
|
const recentReadyNotifications: RecentNotifications = new Map()
|
|
174
171
|
const recentPermissionNotifications: RecentNotifications = new Map()
|
|
175
172
|
|
|
@@ -193,7 +190,7 @@ export default Plugin.define({
|
|
|
193
190
|
const sendNotification = async (
|
|
194
191
|
title: string,
|
|
195
192
|
message: string,
|
|
196
|
-
soundName:
|
|
193
|
+
soundName: AttentionSoundName
|
|
197
194
|
): Promise<void> => {
|
|
198
195
|
try {
|
|
199
196
|
await context.attention.notify({
|
|
@@ -245,9 +242,9 @@ export default Plugin.define({
|
|
|
245
242
|
})
|
|
246
243
|
)
|
|
247
244
|
|
|
248
|
-
// Session
|
|
245
|
+
// Session execution failed
|
|
249
246
|
unsubscribers.push(
|
|
250
|
-
context.data.on("session.
|
|
247
|
+
context.data.on("session.execution.failed", async (event) => {
|
|
251
248
|
const sessionID = toNonEmptyString(event.data.sessionID)
|
|
252
249
|
if (!sessionID) return
|
|
253
250
|
|
|
@@ -260,12 +257,7 @@ export default Plugin.define({
|
|
|
260
257
|
if (isQuietHours(config)) return
|
|
261
258
|
|
|
262
259
|
const error = event.data.error
|
|
263
|
-
const errorMessage =
|
|
264
|
-
typeof error === "string"
|
|
265
|
-
? error.slice(0, 100)
|
|
266
|
-
: error
|
|
267
|
-
? String(error).slice(0, 100)
|
|
268
|
-
: "Something went wrong"
|
|
260
|
+
const errorMessage = (error.message ?? "Something went wrong").slice(0, 100)
|
|
269
261
|
|
|
270
262
|
await sendNotification(
|
|
271
263
|
"Something went wrong",
|
|
@@ -303,16 +295,16 @@ export default Plugin.define({
|
|
|
303
295
|
})
|
|
304
296
|
)
|
|
305
297
|
|
|
306
|
-
// Permission
|
|
298
|
+
// Permission replied (when user responds)
|
|
307
299
|
unsubscribers.push(
|
|
308
|
-
context.data.on("permission.
|
|
300
|
+
context.data.on("permission.replied", async (event) => {
|
|
309
301
|
// Check quiet hours
|
|
310
302
|
if (isQuietHours(config)) return
|
|
311
303
|
|
|
312
304
|
// Deduplication
|
|
313
|
-
const permissionKey = toNonEmptyString(event.data.
|
|
314
|
-
? `permission:
|
|
315
|
-
: `permission-
|
|
305
|
+
const permissionKey = toNonEmptyString(event.data.requestID)
|
|
306
|
+
? `permission:replied:${event.data.requestID}`
|
|
307
|
+
: `permission-reply:${Date.now()}`
|
|
316
308
|
if (
|
|
317
309
|
!shouldSendDedupedNotification(
|
|
318
310
|
recentPermissionNotifications,
|
|
@@ -331,35 +323,6 @@ export default Plugin.define({
|
|
|
331
323
|
})
|
|
332
324
|
)
|
|
333
325
|
|
|
334
|
-
// Question asked (when AI asks a question)
|
|
335
|
-
unsubscribers.push(
|
|
336
|
-
context.data.on("question.asked", async (event) => {
|
|
337
|
-
// Check quiet hours
|
|
338
|
-
if (isQuietHours(config)) return
|
|
339
|
-
|
|
340
|
-
// Deduplication
|
|
341
|
-
const questionKey = toNonEmptyString(event.data.id)
|
|
342
|
-
? `question:request:${event.data.id}`
|
|
343
|
-
: `question:${Date.now()}`
|
|
344
|
-
if (
|
|
345
|
-
!shouldSendDedupedNotification(
|
|
346
|
-
recentQuestionNotifications,
|
|
347
|
-
questionKey,
|
|
348
|
-
QUESTION_DEDUPE_WINDOW_MS
|
|
349
|
-
)
|
|
350
|
-
) {
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const sound = config.sounds.question ?? config.sounds.permission
|
|
355
|
-
await sendNotification(
|
|
356
|
-
"Question for you",
|
|
357
|
-
"OpenCode needs your input",
|
|
358
|
-
sound
|
|
359
|
-
)
|
|
360
|
-
})
|
|
361
|
-
)
|
|
362
|
-
|
|
363
326
|
// Return cleanup function
|
|
364
327
|
return () => {
|
|
365
328
|
unsubscribers.forEach((unsub) => unsub())
|