opencode-discord 1.8.1 → 1.8.3

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 +112 -35
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -5,14 +5,16 @@ const CLIENT_ID = process.env.DISCORD_CLIENT_ID ?? "1486144419940929676"
5
5
  const RECONNECT_DELAY = 5_000
6
6
  const MAX_RECONNECT_ATTEMPTS = 5
7
7
 
8
- type Activity = "idle" | "thinking" | "editing" | "running" | "reading"
9
- type LogLevel = "debug" | "info" | "error" | "warn"
8
+ type Activity = "idle" | "thinking" | "editing" | "running" | "reading" | "approving" | "commanding"
10
9
 
11
10
  function basename(filePath?: string): string | undefined {
12
11
  if (!filePath) return undefined
13
12
  const normalized = filePath.replace(/\\/g, "/")
14
- const name = normalized.split("/").pop()
15
- return name || undefined
13
+ return normalized.split("/").pop() || undefined
14
+ }
15
+
16
+ function props(event: { properties?: Record<string, unknown> }) {
17
+ return (event.properties ?? {}) as Record<string, any>
16
18
  }
17
19
 
18
20
  export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
@@ -24,26 +26,39 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
24
26
  let lastEditedFile: string | undefined
25
27
  let activity: Activity = "idle"
26
28
  let sessionCount = 0
29
+ let filesChanged = 0
30
+ let lastCommand: string | undefined
31
+ let modelName: string | undefined
32
+ let sessionTokens = { input: 0, output: 0 }
27
33
  let reconnectAttempts = 0
28
34
  let reconnectTimer: ReturnType<typeof setTimeout> | undefined
29
35
  let pendingPresence = false
36
+ let presenceDirty = false
30
37
  let destroyed = false
31
38
 
32
- function log(level: LogLevel, message: string, extra?: Record<string, unknown>) {
33
- sdk.app.log({
34
- body: {
35
- service: "discord-status",
36
- level,
37
- message,
38
- extra,
39
- },
40
- }).catch(() => {})
39
+ function log(level: string, message: string, extra?: Record<string, unknown>) {
40
+ sdk.app.log({ body: { service: "discord-status", level, message, extra } }).catch(() => {})
41
41
  }
42
42
 
43
- log("info", "Plugin initializing", { clientId: CLIENT_ID, project: currentProject })
43
+ function formatTokens(n: number): string {
44
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
45
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
46
+ return String(n)
47
+ }
48
+
49
+ function resetSession() {
50
+ lastEditedFile = undefined
51
+ lastCommand = undefined
52
+ filesChanged = 0
53
+ sessionTokens = { input: 0, output: 0 }
54
+ }
44
55
 
45
56
  function updatePresence() {
46
- if (!connected || pendingPresence) return
57
+ if (!connected) return
58
+ if (pendingPresence) {
59
+ presenceDirty = true
60
+ return
61
+ }
47
62
  pendingPresence = true
48
63
 
49
64
  let details: string
@@ -70,13 +85,24 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
70
85
  case "reading":
71
86
  details = lastEditedFile ? `Reading ${lastEditedFile}` : "Reading files"
72
87
  break
88
+ case "approving":
89
+ details = "Waiting for approval..."
90
+ break
91
+ case "commanding":
92
+ details = lastCommand ? `Running /${lastCommand}` : "Running command"
93
+ break
73
94
  default:
74
95
  details = "Session active"
75
96
  }
76
97
 
77
- state = currentProject
78
- smallImageKey = activity === "thinking" ? "thinking" : "active"
79
- smallImageText = activity === "thinking" ? "Generating" : "Active"
98
+ const stateParts = [currentProject]
99
+ if (filesChanged > 0) stateParts.push(`${filesChanged} file${filesChanged !== 1 ? "s" : ""} changed`)
100
+ const totalTokens = sessionTokens.input + sessionTokens.output
101
+ if (totalTokens > 0) stateParts.push(`${formatTokens(totalTokens)} tokens`)
102
+ state = stateParts.join(" · ")
103
+
104
+ smallImageKey = activity === "thinking" ? "thinking" : activity === "approving" ? "idle" : "active"
105
+ smallImageText = activity === "thinking" ? "Generating" : activity === "approving" ? "Needs approval" : "Active"
80
106
  }
81
107
 
82
108
  rpc.user?.setActivity({
@@ -84,13 +110,21 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
84
110
  state,
85
111
  startTimestamp,
86
112
  largeImageKey: "opencode",
87
- largeImageText: `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
113
+ largeImageText: modelName
114
+ ? `OpenCode · ${modelName}`
115
+ : `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
88
116
  smallImageKey,
89
117
  smallImageText,
90
118
  })
91
119
  .then(() => log("debug", "Presence updated", { details, state }))
92
120
  .catch((e) => log("error", "Failed to set presence", { error: String(e) }))
93
- .finally(() => { pendingPresence = false })
121
+ .finally(() => {
122
+ pendingPresence = false
123
+ if (presenceDirty) {
124
+ presenceDirty = false
125
+ updatePresence()
126
+ }
127
+ })
94
128
  }
95
129
 
96
130
  function scheduleReconnect() {
@@ -135,8 +169,8 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
135
169
  rpc.destroy().catch(() => {})
136
170
  })
137
171
 
172
+ log("info", "Logging in to Discord RPC...", { clientId: CLIENT_ID, project: currentProject })
138
173
  try {
139
- log("info", "Logging in to Discord RPC...")
140
174
  await rpc.login()
141
175
  reconnectAttempts = 0
142
176
  log("info", "Discord RPC login successful")
@@ -153,9 +187,23 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
153
187
  sessionActive = true
154
188
  sessionCount += 1
155
189
  startTimestamp = new Date()
156
- lastEditedFile = undefined
190
+ resetSession()
157
191
  activity = "thinking"
158
192
  break
193
+ case "session.updated":
194
+ sessionActive = true
195
+ if (activity === "idle") activity = "thinking"
196
+ break
197
+ case "session.status": {
198
+ const status = props(event).status
199
+ if (status === "busy" || status === "retry") {
200
+ sessionActive = true
201
+ activity = "thinking"
202
+ } else if (status === "idle") {
203
+ activity = "idle"
204
+ }
205
+ break
206
+ }
159
207
  case "session.idle":
160
208
  activity = "idle"
161
209
  break
@@ -165,26 +213,59 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
165
213
  break
166
214
  case "session.deleted":
167
215
  sessionActive = false
168
- lastEditedFile = undefined
216
+ resetSession()
169
217
  activity = "idle"
170
- if (connected) {
171
- rpc.user?.clearActivity().catch(() => {})
172
- }
218
+ if (connected) rpc.user?.clearActivity().catch(() => {})
173
219
  return
174
220
  case "session.compacted":
175
221
  activity = "thinking"
176
222
  break
177
- case "file.edited": {
223
+ case "file.edited":
178
224
  activity = "editing"
179
- const editedFile = (event as { properties?: { file?: string } }).properties?.file
180
- lastEditedFile = basename(editedFile) ?? lastEditedFile
225
+ lastEditedFile = basename(props(event).file) ?? lastEditedFile
226
+ break
227
+ case "session.diff": {
228
+ const diffs = props(event).diff
229
+ if (Array.isArray(diffs)) filesChanged = diffs.length
230
+ break
231
+ }
232
+ case "message.part.updated": {
233
+ const part = props(event).part
234
+ if (part?.type === "step-finish" && part.tokens) {
235
+ sessionTokens.input += part.tokens.input ?? 0
236
+ sessionTokens.output += part.tokens.output ?? 0
237
+ }
238
+ if (sessionActive && activity !== "editing" && activity !== "running") {
239
+ activity = "thinking"
240
+ }
181
241
  break
182
242
  }
243
+ case "pty.created":
244
+ activity = "running"
245
+ break
246
+ case "pty.exited":
247
+ activity = sessionActive ? "thinking" : "idle"
248
+ break
249
+ case "permission.updated":
250
+ activity = "approving"
251
+ break
252
+ case "permission.replied":
253
+ activity = sessionActive ? "thinking" : "idle"
254
+ break
255
+ case "command.executed":
256
+ lastCommand = props(event).name
257
+ activity = "commanding"
258
+ break
183
259
  }
184
260
 
185
261
  updatePresence()
186
262
  },
187
263
 
264
+ "chat.params": async (input) => {
265
+ const name = input.model?.name ?? input.model?.id
266
+ if (name) modelName = name
267
+ },
268
+
188
269
  "tool.execute.before": async (input, output) => {
189
270
  const args = output.args as { filePath?: string } | undefined
190
271
 
@@ -202,15 +283,11 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
202
283
  },
203
284
 
204
285
  "tool.execute.after": async (input) => {
205
- if (input.tool === "edit" || input.tool === "write") {
206
- activity = "editing"
207
- const filePath = input.args?.filePath as string | undefined
208
- lastEditedFile = basename(filePath) ?? lastEditedFile
209
- } else if (input.tool === "bash") {
286
+ if (input.tool === "bash") {
210
287
  activity = sessionActive ? "thinking" : "idle"
211
288
  }
212
289
 
213
290
  updatePresence()
214
291
  },
215
292
  }
216
- }
293
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
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",