#418 · @enchantednatures · opened Jun 26, 2025 at 3:12 PM UTC · last updated Mar 21, 2026 at 12:16 PM UTC

feat(Discord): add rich presence support

appfeat
34
+68577 files

Score breakdown

Impact

4.0

Clarity

5.0

Urgency

2.0

Ease Of Review

2.0

Guidelines

3.0

Readiness

0.0

Size

1.0

Trust

5.0

Traction

6.0

Summary

This PR introduces Discord rich presence support but is incomplete. It's an initial effort lacking functional share links, a bot ID, and necessary images. It is not merge-ready.

Open in GitHub

Description

initial effort on getting rich presence working. I haven't been able to make the share link show up. I'll also need a Discord bot id to plug in to use as default as well as a few images #https://github.com/sst/opencode/issues/406

Linked Issues

None.

Comments

PR comments

enchantednatures

Some notes here:

this is using @xhayper/discord-rpc rather than the discordjs/rpc library, as I could not get the later to connect. I was mostly interested in prototyping it out to see if it would work, this would probably require a custom rpc implementation, other than just drop in a library

enchantednatures

<img width="276" alt="Screenshot 2025-07-01 at 11 09 42 PM" src="https://github.com/user-attachments/assets/c21309ad-4572-4719-b344-fef0d92f3927" /> <img width="282" alt="Screenshot 2025-07-01 at 11 15 44 PM" src="https://github.com/user-attachments/assets/bd341fbf-0887-437a-9ed9-fcbdb5104200" />

Unfortunately, using join secrets require a launch command and setting the share url only works for YouTube and Twitch urls.

adamdotdevin

hey @enchantednatures curious, where is this at?

enchantednatures

hey @enchantednatures curious, where is this at?

I should have some time this week to update it.

Unfortunately, I could not get the /share command to create a join link which was the feature I was most excited for. However, I was able to get it to show the model in use and how long the opencode session had been running.

mufarodev

Hey!

What's the progress with this PR? Would love to use it.

Edit: Yeah the age of the PR created a few conflicts :/

Microck

any updates on this?

Changed Files

STATS.md

+2655
@@ -1,7 +1,267 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | --------------- | --------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | --------------------- | -------------------- | --------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,788 (+2,680) | 46,168 (+2,423) | 70,956 (+5,103) |
| 2025-07-03 | 27,814 (+3,026) | 49,955 (+3,787) | 77,769 (+6,813) |
| 2025-07-04 | 30,599 (+2,785) | 54,758 (+4,803) | 85,357 (+7,588) |
| 2025-07-05 | 32,539 (+1,940) | 58,371 (+3,613) | 90,910 (+5,553) |
| 2025-07-06 | 33,775 (

bun.lock

+292
@@ -30,6 +30,7 @@
"@hono/zod-validator": "0.5.0",
"@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"@xhayper/discord-rpc": "1.2.2",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
@@ -214,6 +215,12 @@
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
"@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="],
"@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ

packages/opencode/config.schema.json

+400
@@ -355,6 +355,46 @@
}
},
"additionalProperties": false
},
"discord": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable Discord Rich Presence"
},
"applicationId": {
"type": "string",
"description": "Discord application ID for Rich Presence"
},
"showModel": {
"type": "boolean",
"default": true,
"description": "Show current AI model in presence"
},
"showProject": {
"type": "boolean",
"default": true,
"description": "Show current project name in presence"
},
"showSession": {
"type": "boolean",
"default": true,
"description": "Show session information in presence"
},
"showShareUrl": {
"type": "boolean",
"default": true,
"description": "Show share URL button when session is shared"
},
"customStatus": {
"type": "string",
"description": "Custom status text to displ

packages/opencode/package.json

+10
@@ -31,6 +31,7 @@
"@hono/zod-validator": "0.5.0",
"@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"@xhayper/discord-rpc": "1.2.2",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",

packages/opencode/src/cli/bootstrap.ts

+20
@@ -1,5 +1,6 @@
import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { Discord } from "../discord/discord"
import { FileWatcher } from "../file/watch"
import { Format } from "../format"
import { LSP } from "../lsp"
@@ -14,6 +15,7 @@ export async function bootstrap<T>(
Format.init()
ConfigHooks.init()
LSP.init()
Discord.init()
FileWatcher.init()
return cb(app)

packages/opencode/src/config/config.ts

+280
@@ -10,6 +10,7 @@ import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -202,6 +203,33 @@ export namespace Config {
.optional(),
})
.optional(),
discord: z
.object({
enabled: z.boolean().default(false).describe("Enable Discord Rich Presence"),
applicationId: z
.string()
.optional()
.describe("Discord application ID for Rich Presence"),
showModel: z
.boolean()
.default(true)
.describe("Show current AI model in presence"),
showProject: z
.boolean()
.default(true)
.describe("Show current project name in presence"),
showSession: z
.boolean()
.default(true)
.describe("Show session information in presence"),
customStatus: z
.string()
.optional()
.describe("Custom status text to display"),
})
.strict()
.optional()

packages/opencode/src/discord/discord.ts

+3200
@@ -0,0 +1,320 @@
import { z } from "zod"
import { App } from "../app/app"
import { Bus } from "../bus"
import { Session } from "../session"
import { Message } from "../session/message"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import { Config } from "../config/config"
import { ActivitySupportedPlatform, Client } from "@xhayper/discord-rpc"
import path from "path"
export namespace Discord {
const log = Log.create({ service: "discord" })
const DEFAULT_APPLICATION_ID = "1388540337164910712"
export type Config = NonNullable<Config.Info["discord"]>
interface PresenceData {
details?: string
state?: string
startTimestamp?: number
largeImageKey?: string
largeImageText?: string
smallImageKey?: string
smallImageText?: string
supportedPlatforms?: (
| ActivitySupportedPlatform
| `${ActivitySupportedPlatform}`
)[]
instance?: boolean
}
const state = App.state(
"discord",
async () => {
const configData = await Config.get()
const config = configData.discord
if (!config?.enabled) {
return {
client: null as Clien