opencode-discord 1.8.2 → 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 +66 -46
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -6,13 +6,15 @@ const RECONNECT_DELAY = 5_000
6
6
  const MAX_RECONNECT_ATTEMPTS = 5
7
7
 
8
8
  type Activity = "idle" | "thinking" | "editing" | "running" | "reading" | "approving" | "commanding"
9
- type LogLevel = "debug" | "info" | "error" | "warn"
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 }) => {
@@ -26,26 +28,37 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
26
28
  let sessionCount = 0
27
29
  let filesChanged = 0
28
30
  let lastCommand: string | undefined
31
+ let modelName: string | undefined
32
+ let sessionTokens = { input: 0, output: 0 }
29
33
  let reconnectAttempts = 0
30
34
  let reconnectTimer: ReturnType<typeof setTimeout> | undefined
31
35
  let pendingPresence = false
36
+ let presenceDirty = false
32
37
  let destroyed = false
33
38
 
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(() => {})
39
+ function log(level: string, message: string, extra?: Record<string, unknown>) {
40
+ sdk.app.log({ body: { service: "discord-status", level, message, extra } }).catch(() => {})
43
41
  }
44
42
 
45
- 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
+ }
46
55
 
47
56
  function updatePresence() {
48
- if (!connected || pendingPresence) return
57
+ if (!connected) return
58
+ if (pendingPresence) {
59
+ presenceDirty = true
60
+ return
61
+ }
49
62
  pendingPresence = true
50
63
 
51
64
  let details: string
@@ -82,9 +95,12 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
82
95
  details = "Session active"
83
96
  }
84
97
 
85
- state = filesChanged > 0
86
- ? `${currentProject} · ${filesChanged} file${filesChanged !== 1 ? "s" : ""} changed`
87
- : currentProject
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
+
88
104
  smallImageKey = activity === "thinking" ? "thinking" : activity === "approving" ? "idle" : "active"
89
105
  smallImageText = activity === "thinking" ? "Generating" : activity === "approving" ? "Needs approval" : "Active"
90
106
  }
@@ -94,13 +110,21 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
94
110
  state,
95
111
  startTimestamp,
96
112
  largeImageKey: "opencode",
97
- largeImageText: `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
113
+ largeImageText: modelName
114
+ ? `OpenCode · ${modelName}`
115
+ : `OpenCode · ${sessionCount} session${sessionCount !== 1 ? "s" : ""}`,
98
116
  smallImageKey,
99
117
  smallImageText,
100
118
  })
101
119
  .then(() => log("debug", "Presence updated", { details, state }))
102
120
  .catch((e) => log("error", "Failed to set presence", { error: String(e) }))
103
- .finally(() => { pendingPresence = false })
121
+ .finally(() => {
122
+ pendingPresence = false
123
+ if (presenceDirty) {
124
+ presenceDirty = false
125
+ updatePresence()
126
+ }
127
+ })
104
128
  }
105
129
 
106
130
  function scheduleReconnect() {
@@ -145,8 +169,8 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
145
169
  rpc.destroy().catch(() => {})
146
170
  })
147
171
 
172
+ log("info", "Logging in to Discord RPC...", { clientId: CLIENT_ID, project: currentProject })
148
173
  try {
149
- log("info", "Logging in to Discord RPC...")
150
174
  await rpc.login()
151
175
  reconnectAttempts = 0
152
176
  log("info", "Discord RPC login successful")
@@ -163,9 +187,7 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
163
187
  sessionActive = true
164
188
  sessionCount += 1
165
189
  startTimestamp = new Date()
166
- lastEditedFile = undefined
167
- lastCommand = undefined
168
- filesChanged = 0
190
+ resetSession()
169
191
  activity = "thinking"
170
192
  break
171
193
  case "session.updated":
@@ -173,7 +195,7 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
173
195
  if (activity === "idle") activity = "thinking"
174
196
  break
175
197
  case "session.status": {
176
- const status = (event as { properties?: { status?: string } }).properties?.status
198
+ const status = props(event).status
177
199
  if (status === "busy" || status === "retry") {
178
200
  sessionActive = true
179
201
  activity = "thinking"
@@ -191,29 +213,28 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
191
213
  break
192
214
  case "session.deleted":
193
215
  sessionActive = false
194
- lastEditedFile = undefined
195
- lastCommand = undefined
196
- filesChanged = 0
216
+ resetSession()
197
217
  activity = "idle"
198
- if (connected) {
199
- rpc.user?.clearActivity().catch(() => {})
200
- }
218
+ if (connected) rpc.user?.clearActivity().catch(() => {})
201
219
  return
202
220
  case "session.compacted":
203
221
  activity = "thinking"
204
222
  break
205
- case "file.edited": {
223
+ case "file.edited":
206
224
  activity = "editing"
207
- const editedFile = (event as { properties?: { file?: string } }).properties?.file
208
- lastEditedFile = basename(editedFile) ?? lastEditedFile
225
+ lastEditedFile = basename(props(event).file) ?? lastEditedFile
209
226
  break
210
- }
211
227
  case "session.diff": {
212
- const diffs = (event as { properties?: { diff?: unknown[] } }).properties?.diff
213
- if (diffs) filesChanged = diffs.length
228
+ const diffs = props(event).diff
229
+ if (Array.isArray(diffs)) filesChanged = diffs.length
214
230
  break
215
231
  }
216
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
+ }
217
238
  if (sessionActive && activity !== "editing" && activity !== "running") {
218
239
  activity = "thinking"
219
240
  }
@@ -231,17 +252,20 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
231
252
  case "permission.replied":
232
253
  activity = sessionActive ? "thinking" : "idle"
233
254
  break
234
- case "command.executed": {
235
- const cmdName = (event as { properties?: { name?: string } }).properties?.name
236
- lastCommand = cmdName
255
+ case "command.executed":
256
+ lastCommand = props(event).name
237
257
  activity = "commanding"
238
258
  break
239
- }
240
259
  }
241
260
 
242
261
  updatePresence()
243
262
  },
244
263
 
264
+ "chat.params": async (input) => {
265
+ const name = input.model?.name ?? input.model?.id
266
+ if (name) modelName = name
267
+ },
268
+
245
269
  "tool.execute.before": async (input, output) => {
246
270
  const args = output.args as { filePath?: string } | undefined
247
271
 
@@ -259,15 +283,11 @@ export const DiscordStatus: Plugin = async ({ client: sdk, directory }) => {
259
283
  },
260
284
 
261
285
  "tool.execute.after": async (input) => {
262
- if (input.tool === "edit" || input.tool === "write") {
263
- activity = "editing"
264
- const filePath = input.args?.filePath as string | undefined
265
- lastEditedFile = basename(filePath) ?? lastEditedFile
266
- } else if (input.tool === "bash") {
286
+ if (input.tool === "bash") {
267
287
  activity = sessionActive ? "thinking" : "idle"
268
288
  }
269
289
 
270
290
  updatePresence()
271
291
  },
272
292
  }
273
- }
293
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord",
3
- "version": "1.8.2",
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",