opencode-discord 1.8.0 → 1.8.2

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.
Files changed (2) hide show
  1. package/index.ts +138 -50
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -2,8 +2,10 @@ import type { Plugin } from "@opencode-ai/plugin"
2
2
  import { Client } from "@xhayper/discord-rpc"
3
3
 
4
4
  const CLIENT_ID = process.env.DISCORD_CLIENT_ID ?? "1486144419940929676"
5
+ const RECONNECT_DELAY = 5_000
6
+ const MAX_RECONNECT_ATTEMPTS = 5
5
7
 
6
- type Activity = "idle" | "thinking" | "editing" | "running" | "reading"
8
+ type Activity = "idle" | "thinking" | "editing" | "running" | "reading" | "approving" | "commanding"
7
9
  type LogLevel = "debug" | "info" | "error" | "warn"
8
10
 
9
11
  function basename(filePath?: string): string | undefined {
@@ -18,28 +20,33 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
18
20
  let connected = false
19
21
  let sessionActive = false
20
22
  const currentProject = basename(directory) ?? "Unknown Project"
21
- let startTimestamp = Date.now()
23
+ let startTimestamp = new Date()
22
24
  let lastEditedFile: string | undefined
23
25
  let activity: Activity = "idle"
24
26
  let sessionCount = 0
27
+ let filesChanged = 0
28
+ let lastCommand: string | undefined
29
+ let reconnectAttempts = 0
30
+ let reconnectTimer: ReturnType<typeof setTimeout> | undefined
31
+ let pendingPresence = false
32
+ let destroyed = false
25
33
 
26
- async function log(level: LogLevel, message: string, extra?: Record<string, unknown>) {
27
- try {
28
- await sdk.app.log({
29
- body: {
30
- service: "discord-status",
31
- level,
32
- message,
33
- extra,
34
- },
35
- })
36
- } catch { }
34
+ function log(level: LogLevel, message: string, extra?: Record<string, unknown>) {
35
+ sdk.app.log({
36
+ body: {
37
+ service: "discord-status",
38
+ level,
39
+ message,
40
+ extra,
41
+ },
42
+ }).catch(() => {})
37
43
  }
38
44
 
39
- await log("info", "Plugin initializing", { clientId: CLIENT_ID, project: currentProject })
45
+ log("info", "Plugin initializing", { clientId: CLIENT_ID, project: currentProject })
40
46
 
41
- async function updatePresence() {
42
- if (!connected) return
47
+ function updatePresence() {
48
+ if (!connected || pendingPresence) return
49
+ pendingPresence = true
43
50
 
44
51
  let details: string
45
52
  let state: string
@@ -65,70 +72,117 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
65
72
  case "reading":
66
73
  details = lastEditedFile ? `Reading ${lastEditedFile}` : "Reading files"
67
74
  break
75
+ case "approving":
76
+ details = "Waiting for approval..."
77
+ break
78
+ case "commanding":
79
+ details = lastCommand ? `Running /${lastCommand}` : "Running command"
80
+ break
68
81
  default:
69
82
  details = "Session active"
70
83
  }
71
84
 
72
- state = currentProject
73
- smallImageKey = activity === "thinking" ? "thinking" : "active"
74
- smallImageText = activity === "thinking" ? "Generating" : "Active"
85
+ state = filesChanged > 0
86
+ ? `${currentProject} · ${filesChanged} file${filesChanged !== 1 ? "s" : ""} changed`
87
+ : currentProject
88
+ smallImageKey = activity === "thinking" ? "thinking" : activity === "approving" ? "idle" : "active"
89
+ smallImageText = activity === "thinking" ? "Generating" : activity === "approving" ? "Needs approval" : "Active"
75
90
  }
76
91
 
77
- try {
78
- await rpc.user?.setActivity({
79
- details,
80
- state,
81
- startTimestamp,
82
- largeImageKey: "opencode",
83
- largeImageText: `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
84
- smallImageKey,
85
- smallImageText,
86
- })
87
- await log("debug", "Presence updated", { details, state })
88
- } catch (e) {
89
- await log("error", "Failed to set presence", { error: String(e) })
92
+ rpc.user?.setActivity({
93
+ details,
94
+ state,
95
+ startTimestamp,
96
+ largeImageKey: "opencode",
97
+ largeImageText: `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
98
+ smallImageKey,
99
+ smallImageText,
100
+ })
101
+ .then(() => log("debug", "Presence updated", { details, state }))
102
+ .catch((e) => log("error", "Failed to set presence", { error: String(e) }))
103
+ .finally(() => { pendingPresence = false })
104
+ }
105
+
106
+ function scheduleReconnect() {
107
+ if (destroyed || reconnectTimer) return
108
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
109
+ log("error", "Max reconnect attempts reached, giving up")
110
+ return
90
111
  }
112
+ reconnectAttempts++
113
+ log("info", "Scheduling reconnect", { attempt: reconnectAttempts })
114
+ reconnectTimer = setTimeout(() => {
115
+ reconnectTimer = undefined
116
+ rpc.login()
117
+ .then(() => {
118
+ log("info", "Reconnected to Discord RPC")
119
+ reconnectAttempts = 0
120
+ })
121
+ .catch((e) => {
122
+ log("error", "Reconnect failed", { error: String(e) })
123
+ scheduleReconnect()
124
+ })
125
+ }, RECONNECT_DELAY)
91
126
  }
92
127
 
93
- rpc.on("ready", async () => {
128
+ rpc.on("ready", () => {
94
129
  connected = true
95
- startTimestamp = Date.now()
96
- await log("info", "Discord RPC connected")
97
- await updatePresence()
130
+ startTimestamp = new Date()
131
+ log("info", "Discord RPC connected")
132
+ updatePresence()
98
133
  })
99
134
 
100
- rpc.on("error", async (err: any) => {
101
- await log("error", "Discord RPC error", { error: String(err) })
135
+ rpc.on("disconnected", () => {
136
+ connected = false
137
+ log("warn", "Discord RPC disconnected")
138
+ scheduleReconnect()
102
139
  })
103
140
 
104
- rpc.on("disconnected", async () => {
105
- connected = false
106
- await log("warn", "Discord RPC disconnected")
141
+ process.on("exit", () => {
142
+ destroyed = true
143
+ if (reconnectTimer) clearTimeout(reconnectTimer)
144
+ if (connected) rpc.user?.clearActivity().catch(() => {})
145
+ rpc.destroy().catch(() => {})
107
146
  })
108
147
 
109
148
  try {
110
- await log("info", "Logging in to Discord RPC...")
149
+ log("info", "Logging in to Discord RPC...")
111
150
  await rpc.login()
112
- await log("info", "Discord RPC login successful")
151
+ reconnectAttempts = 0
152
+ log("info", "Discord RPC login successful")
113
153
  } catch (e) {
114
154
  connected = false
115
- await log("error", "Discord RPC login failed", { error: String(e) })
155
+ log("error", "Discord RPC login failed", { error: String(e) })
156
+ scheduleReconnect()
116
157
  }
117
158
 
118
159
  return {
119
160
  event: async ({ event }) => {
120
- const eventType = event.type as string
121
-
122
- switch (eventType) {
161
+ switch (event.type) {
123
162
  case "session.created":
124
163
  sessionActive = true
125
164
  sessionCount += 1
126
- startTimestamp = Date.now()
165
+ startTimestamp = new Date()
127
166
  lastEditedFile = undefined
167
+ lastCommand = undefined
168
+ filesChanged = 0
128
169
  activity = "thinking"
129
170
  break
130
- case "session.idle":
171
+ case "session.updated":
131
172
  sessionActive = true
173
+ if (activity === "idle") activity = "thinking"
174
+ break
175
+ case "session.status": {
176
+ const status = (event as { properties?: { status?: string } }).properties?.status
177
+ if (status === "busy" || status === "retry") {
178
+ sessionActive = true
179
+ activity = "thinking"
180
+ } else if (status === "idle") {
181
+ activity = "idle"
182
+ }
183
+ break
184
+ }
185
+ case "session.idle":
132
186
  activity = "idle"
133
187
  break
134
188
  case "session.error":
@@ -138,8 +192,13 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
138
192
  case "session.deleted":
139
193
  sessionActive = false
140
194
  lastEditedFile = undefined
195
+ lastCommand = undefined
196
+ filesChanged = 0
141
197
  activity = "idle"
142
- break
198
+ if (connected) {
199
+ rpc.user?.clearActivity().catch(() => {})
200
+ }
201
+ return
143
202
  case "session.compacted":
144
203
  activity = "thinking"
145
204
  break
@@ -149,6 +208,35 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
149
208
  lastEditedFile = basename(editedFile) ?? lastEditedFile
150
209
  break
151
210
  }
211
+ case "session.diff": {
212
+ const diffs = (event as { properties?: { diff?: unknown[] } }).properties?.diff
213
+ if (diffs) filesChanged = diffs.length
214
+ break
215
+ }
216
+ case "message.part.updated": {
217
+ if (sessionActive && activity !== "editing" && activity !== "running") {
218
+ activity = "thinking"
219
+ }
220
+ break
221
+ }
222
+ case "pty.created":
223
+ activity = "running"
224
+ break
225
+ case "pty.exited":
226
+ activity = sessionActive ? "thinking" : "idle"
227
+ break
228
+ case "permission.updated":
229
+ activity = "approving"
230
+ break
231
+ case "permission.replied":
232
+ activity = sessionActive ? "thinking" : "idle"
233
+ break
234
+ case "command.executed": {
235
+ const cmdName = (event as { properties?: { name?: string } }).properties?.name
236
+ lastCommand = cmdName
237
+ activity = "commanding"
238
+ break
239
+ }
152
240
  }
153
241
 
154
242
  updatePresence()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Discord Rich Presence plugin for OpenCode — shows your AI coding activity in Discord",
5
5
  "type": "module",
6
6
  "main": "index.ts",