Skip to content

feat(code): add Discord Rich Presence integration#2453

Open
gantoine wants to merge 5 commits into
mainfrom
feat/discord-presence
Open

feat(code): add Discord Rich Presence integration#2453
gantoine wants to merge 5 commits into
mainfrom
feat/discord-presence

Conversation

@gantoine
Copy link
Copy Markdown
Member

@gantoine gantoine commented Jun 1, 2026

Summary

Adds a Discord Rich Presence integration that surfaces the user's current activity in their Discord status.

  • Main-process service (discord-presence/service.ts) talking to Discord over its local IPC socket (discord-ipc.ts), with presence formatting helpers (presence-format.ts) and Zod schemas (schemas.ts).
  • tRPC router (routers/discord-presence.ts) exposing the service, wired into the root router and DI container.
  • Settings persistence via settingsStore plus a new Discord settings section in the renderer (DiscordSettings.tsx), gated by a settings dialog entry.
  • Subscription registrar (renderer/features/discord-presence/subscriptions.ts) wired once at app boot.
  • New .env.example entries and a vite.main.config.mts tweak for the integration.

Screenshots

Running Idle Settings
Screenshot 2026-06-02 at 3 28 17 PM Screenshot 2026-06-02 at 3 28 55 PM Screenshot 2026-06-02 at 4 11 56 PM

Test plan

  • presence-format.test.ts unit tests
  • pnpm typecheck (ran via pre-commit hook)
  • Manual: enable Discord presence in settings with Discord running and confirm status updates

🤖 Generated with Claude Code

gantoine and others added 3 commits June 1, 2026 16:06
Add a Discord Rich Presence service that surfaces current activity in
the user's Discord status. Includes a main-process service over Discord
IPC, tRPC router, settings persistence, and a Discord settings section
in the renderer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a DiscordPresencePreview component that mocks the Discord activity
card from app primitives so it tracks the theme. It reacts to the
privacy toggles, offers a Running/Idle switch inline with the Preview
header, shows an elapsing green timer, and dims when the feature is off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gantoine gantoine marked this pull request as ready for review June 2, 2026 20:31
@gantoine gantoine requested a review from Copilot June 2, 2026 20:31
Lock in that a DiscordIpcClient is only ever constructed/connected when
the toggle is enabled: not on boot when disabled, and not when activity
or privacy updates arrive while disabled. Includes an enabled-path sanity
check so the assertions are meaningful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gantoine gantoine requested review from charlesvien and jonathanlab and removed request for charlesvien June 2, 2026 20:32
When Rich Presence is off, the in-settings preview now stops its elapsed
timer, falls back to the idle state (amber pause badge), and locks the
Running/Idle toggle so the dormant integration reads clearly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Comments Outside Diff (1)

  1. apps/code/src/main/services/discord-presence/service.ts, line 588 (link)

    P2 startedAt tracks app-launch time, not current-task time

    startedAt is initialised once in the constructor, so Discord's "elapsed" timer always shows time since the app was launched — not how long the user has been on the current task. A user who opens the app in the morning, does something else, then returns to a task in the afternoon will see a multi-hour counter on their profile. The preview in settings implies "time on this task" (it starts at 3:17), which would mislead users into thinking the service tracks per-task duration.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/code/src/main/services/discord-presence/service.ts
    Line: 588
    
    Comment:
    **`startedAt` tracks app-launch time, not current-task time**
    
    `startedAt` is initialised once in the constructor, so Discord's "elapsed" timer always shows time since the app was launched — not how long the user has been on the current task. A user who opens the app in the morning, does something else, then returns to a task in the afternoon will see a multi-hour counter on their profile. The preview in settings implies "time on this task" (it starts at 3:17), which would mislead users into thinking the service tracks per-task duration.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/main/services/discord-presence/presence-format.test.ts:20-84
**Prefer parameterised tests over individual `it` blocks**

The project convention ("We always prefer parameterised tests") applies here. The seven `it` blocks all exercise the same `buildActivity` function with different intent/options combinations and check expected field values — a textbook candidate for `it.each`. As written, adding a new scenario means another copy-paste block; a `it.each` table makes the test matrix legible and reduces repetition.

### Issue 2 of 3
apps/code/src/main/services/discord-presence/service.ts:588
**`startedAt` tracks app-launch time, not current-task time**

`startedAt` is initialised once in the constructor, so Discord's "elapsed" timer always shows time since the app was launched — not how long the user has been on the current task. A user who opens the app in the morning, does something else, then returns to a task in the afternoon will see a multi-hour counter on their profile. The preview in settings implies "time on this task" (it starts at 3:17), which would mislead users into thinking the service tracks per-task duration.

### Issue 3 of 3
apps/code/src/main/services/discord-presence/presence-format.test.ts:75-83
**Truncation test only bounds-checks length, not content**

The test verifies `length <= 128` but not that the truncation is correct — for example, it doesn't assert that the result ends with `` or that the non-truncated prefix matches. A future change that naively slices to 128 characters (dropping the ellipsis, or inserting extra chars) would still pass this test.

Reviews (1): Last reviewed commit: "feat(discord-presence): pause preview ti..." | Re-trigger Greptile

Comment on lines +20 to +84
describe("buildActivity", () => {
it("hides the task title and repo name by default (privacy-first)", () => {
const activity = buildActivity(activeIntent, baseOptions);
expect(activity.details).toBe("Working on a task");
expect(activity.state).toBe("agent running");
});

it("includes the task title only when opted in", () => {
const activity = buildActivity(activeIntent, {
...baseOptions,
showTaskTitle: true,
});
expect(activity.details).toBe('Working on "Add Discord presence"');
});

it("includes the repo name only when opted in", () => {
const activity = buildActivity(activeIntent, {
...baseOptions,
showRepoName: true,
});
expect(activity.state).toBe("posthog/code · agent running");
});

it("reflects review status with the idle badge when the agent is idle on a task", () => {
const activity = buildActivity(
{ ...activeIntent, agentRunning: false },
{ ...baseOptions, showRepoName: true },
);
expect(activity.state).toBe("posthog/code · reviewing");
expect(activity.assets?.small_image).toBe("posthog_idle");
expect(activity.assets?.small_text).toBe("Reviewing");
});

it("falls back to an idle/browsing presence with the idle badge when no task is focused", () => {
const activity = buildActivity(
{
hasActiveTask: false,
taskTitle: null,
repoName: null,
agentRunning: false,
},
{ ...baseOptions, showTaskTitle: true, showRepoName: true },
);
expect(activity.details).toBe("Idle");
expect(activity.state).toBe("browsing");
expect(activity.assets?.small_image).toBe("posthog_idle");
expect(activity.assets?.small_text).toBe("Idle");
});

it("surfaces the running indicator asset while the agent works", () => {
const activity = buildActivity(activeIntent, baseOptions);
expect(activity.assets?.small_image).toBe("agent_running");
expect(activity.timestamps?.start).toBe(STARTED_AT);
});

it("truncates over-long titles to Discord's field limit", () => {
const longTitle = "x".repeat(200);
const activity = buildActivity(
{ ...activeIntent, taskTitle: longTitle },
{ ...baseOptions, showTaskTitle: true },
);
expect(activity.details).toBeDefined();
expect((activity.details as string).length).toBeLessThanOrEqual(128);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests over individual it blocks

The project convention ("We always prefer parameterised tests") applies here. The seven it blocks all exercise the same buildActivity function with different intent/options combinations and check expected field values — a textbook candidate for it.each. As written, adding a new scenario means another copy-paste block; a it.each table makes the test matrix legible and reduces repetition.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/discord-presence/presence-format.test.ts
Line: 20-84

Comment:
**Prefer parameterised tests over individual `it` blocks**

The project convention ("We always prefer parameterised tests") applies here. The seven `it` blocks all exercise the same `buildActivity` function with different intent/options combinations and check expected field values — a textbook candidate for `it.each`. As written, adding a new scenario means another copy-paste block; a `it.each` table makes the test matrix legible and reduces repetition.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +75 to +83
it("truncates over-long titles to Discord's field limit", () => {
const longTitle = "x".repeat(200);
const activity = buildActivity(
{ ...activeIntent, taskTitle: longTitle },
{ ...baseOptions, showTaskTitle: true },
);
expect(activity.details).toBeDefined();
expect((activity.details as string).length).toBeLessThanOrEqual(128);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Truncation test only bounds-checks length, not content

The test verifies length <= 128 but not that the truncation is correct — for example, it doesn't assert that the result ends with or that the non-truncated prefix matches. A future change that naively slices to 128 characters (dropping the ellipsis, or inserting extra chars) would still pass this test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/discord-presence/presence-format.test.ts
Line: 75-83

Comment:
**Truncation test only bounds-checks length, not content**

The test verifies `length <= 128` but not that the truncation is correct — for example, it doesn't assert that the result ends with `` or that the non-truncated prefix matches. A future change that naively slices to 128 characters (dropping the ellipsis, or inserting extra chars) would still pass this test.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Discord Rich Presence integration to the PostHog Code desktop app, with a main-process Discord IPC client/service, tRPC endpoints, renderer boot-time subscriptions to keep presence in sync, and a new Settings UI section to control privacy and enablement.

Changes:

  • Implement main-process Discord IPC client + presence service (rate-limited updates, reconnect loop, settings persistence, schemas, tests).
  • Add renderer-side boot subscription to derive “presence intent” from navigation/session state and push it to the main service.
  • Add Discord settings section + preview card and wire into Settings dialog navigation.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/code/vite.main.config.mts Exposes VITE_DISCORD_CLIENT_ID to the main build via define.
apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts Adds a new "discord" settings category.
apps/code/src/renderer/features/settings/components/SettingsDialog.tsx Adds Discord section entry/icon/title/component wiring.
apps/code/src/renderer/features/settings/components/sections/DiscordSettings.tsx New Discord settings UI (enable + privacy toggles) driven by tRPC state/subscription.
apps/code/src/renderer/features/settings/components/sections/DiscordPresencePreview.tsx New in-app preview card for Rich Presence appearance.
apps/code/src/renderer/features/discord-presence/subscriptions.ts New renderer subscription registrar to push presence intent to main via tRPC.
apps/code/src/renderer/App.tsx Registers Discord presence subscriptions once at app boot.
apps/code/src/main/trpc/routers/discord-presence.ts New tRPC router exposing Discord presence service controls + status subscription.
apps/code/src/main/trpc/router.ts Wires discordPresence into the root tRPC router.
apps/code/src/main/services/settingsStore.ts Persists Discord presence enablement + privacy flags.
apps/code/src/main/services/discord-presence/service.ts New main-process Discord presence service (connect/reconnect, rate-limit, formatting, state events).
apps/code/src/main/services/discord-presence/service.test.ts Unit tests for connection gating behavior.
apps/code/src/main/services/discord-presence/schemas.ts Zod schemas/types for presence state and intent.
apps/code/src/main/services/discord-presence/presence-format.ts Pure formatter from intent+privacy options to Discord activity payload.
apps/code/src/main/services/discord-presence/presence-format.test.ts Unit tests for formatting/privacy/truncation.
apps/code/src/main/services/discord-presence/discord-ipc.ts New Node net-based Discord local IPC client implementation.
apps/code/src/main/services/discord-presence/constants.ts New constants + getDiscordClientId() helper.
apps/code/src/main/index.ts Eagerly instantiates the Discord presence service at startup.
apps/code/src/main/di/tokens.ts Adds DI token for DiscordPresenceService.
apps/code/src/main/di/container.ts Binds DiscordPresenceService into the DI container.
.env.example Documents VITE_DISCORD_CLIENT_ID setup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +65
export class DiscordIpcClient extends EventEmitter {
private socket: net.Socket | null = null;
private readBuffer = Buffer.alloc(0);
private ready = false;

constructor(private readonly clientId: string) {
super();
}

override on<K extends keyof DiscordIpcClientEvents>(
event: K,
listener: DiscordIpcClientEvents[K],
): this {
return super.on(event, listener);
}

override emit<K extends keyof DiscordIpcClientEvents>(event: K): boolean {
return super.emit(event);
}
new_value: checked,
old_value: enabled,
});
setState((prev) => (prev ? { ...prev, enabled: checked } : prev));
new_value: checked,
old_value: state?.showTaskTitle ?? false,
});
setState((prev) => (prev ? { ...prev, showTaskTitle: checked } : prev));
new_value: checked,
old_value: state?.showRepoName ?? false,
});
setState((prev) => (prev ? { ...prev, showRepoName: checked } : prev));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants