diff --git a/AGENTS.md b/AGENTS.md
index 9e0d066..6dc6a12 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -166,11 +166,12 @@ tests/__snapshots__/ # formatter snapshot golden files (toMatchSnapshot)
`{repo}{#number}: {subject}` (e.g. `acme/widget#7: Add feature`). Repo comes from
`payload.repository.full_name`; fall back to `t("common.repository")` when missing.
- Only the repo head is hyperlinked (never the whole title). Drivers split the title via
- `splitMessageTitle`/`repoUrlFromMessage` (`server/lib/formatters/helpers.ts`): the Discord
- embed title is `{repo}{#number}` linked to the repository URL and `: {subject}` renders as
- the first description line; Telegram keeps the one-line title with an inline repo link and
- a plain subject. Messages without a colon separator (a `:` followed by a space) keep the
- legacy whole-title link — use colon-free wording for such titles.
+ `splitMessageTitle` (`server/lib/formatters/helpers.ts`): the Discord embed title is
+ `{repo}{#number}` linked to the event's object URL (`message.url`, e.g. the issue/PR/comment
+ `html_url`) and `: {subject}` renders as the first description line; Telegram keeps the
+ one-line title with an inline repo link and a plain subject. Messages without a colon
+ separator (a `:` followed by a space) keep the legacy whole-title link — use colon-free
+ wording for such titles.
- Do NOT use `"Comment on org/repo"` / `"Review on org/repo"` prefixes. Comments, reviews
and inline comments use the same `{repo}{#number}: {title}` title as their parent object.
- All event-specific emoji live in `server/lib/formatters/` (via the `emojiPrefix` helper), never in
diff --git a/server/lib/drivers/discord/render.ts b/server/lib/drivers/discord/render.ts
index 6576ae9..cfd5762 100644
--- a/server/lib/drivers/discord/render.ts
+++ b/server/lib/drivers/discord/render.ts
@@ -6,7 +6,6 @@ import {
MAX_FIELD_VALUE,
MAX_FOOTER,
MAX_TITLE,
- repoUrlFromMessage,
splitMessageTitle,
} from "../../formatters/helpers";
@@ -27,10 +26,9 @@ export function renderNeutralMessage(message: NeutralMessage): FormattedMessage
: undefined;
// Discord embed titles can only be linked as a whole, so only the repo head
- // goes into the title (linked to the repository); the `: subject` text is
- // rendered as the first line of the description, unlinked.
+ // goes into the title (linked to the event's object URL); the `: subject`
+ // text is rendered as the first line of the description, unlinked.
const { head, subject } = splitMessageTitle(message.title);
- const repoUrl = repoUrlFromMessage(message.url);
const rawDescription = subject
? message.description
? `${subject}\n${message.description}`
@@ -48,7 +46,7 @@ export function renderNeutralMessage(message: NeutralMessage): FormattedMessage
embeds: [
{
title: cap(head, MAX_TITLE),
- url: subject ? repoUrl : message.url,
+ url: message.url,
color: message.color,
description: cap(rawDescription, MAX_DESCRIPTION) || undefined,
author: message.author
diff --git a/server/lib/drivers/telegram/render.ts b/server/lib/drivers/telegram/render.ts
index fc9d720..80f7bd9 100644
--- a/server/lib/drivers/telegram/render.ts
+++ b/server/lib/drivers/telegram/render.ts
@@ -1,5 +1,5 @@
import type { NeutralMessage } from "../../types";
-import { repoUrlFromMessage, splitMessageTitle } from "../../formatters/helpers";
+import { splitMessageTitle } from "../../formatters/helpers";
/** Telegram caps a single message at 4096 characters (HTML entities included). */
const MAX_TEXT = 4096;
@@ -64,11 +64,11 @@ export function renderNeutralMessage(message: NeutralMessage): string {
const parts: string[] = [];
// HTML allows partial links, so keep the `{repo}{#number}: {subject}` line
- // intact with only the repo head linked (the subject stays plain text).
+ // intact with only the repo head linked to the event's object URL (the
+ // subject stays plain text).
const { head, subject } = splitMessageTitle(message.title);
- const repoUrl = repoUrlFromMessage(message.url);
const title = subject
- ? `${repoUrl ? `${mdToHtml(head)}` : mdToHtml(head)}: ${mdToHtml(subject)}`
+ ? `${message.url ? `${mdToHtml(head)}` : mdToHtml(head)}: ${mdToHtml(subject)}`
: message.url
? `${mdToHtml(message.title)}`
: `${mdToHtml(message.title)}`;
diff --git a/server/lib/formatters/helpers.ts b/server/lib/formatters/helpers.ts
index c8ee428..61b38c7 100644
--- a/server/lib/formatters/helpers.ts
+++ b/server/lib/formatters/helpers.ts
@@ -31,23 +31,6 @@ export function splitMessageTitle(title: string): TitleParts {
return { head: title.slice(0, idx), subject: title.slice(idx + 2) };
}
-/**
- * Repository URL derived from an event URL (origin + owner + repo). Returns
- * undefined when there is no URL to derive from — callers then render the
- * title without a link.
- */
-export function repoUrlFromMessage(url: string | undefined): string | undefined {
- if (!url) return undefined;
- try {
- const parsed = new URL(url);
- const segments = parsed.pathname.split("/").filter(Boolean);
- if (segments.length >= 2) return `${parsed.origin}/${segments[0]}/${segments[1]}`;
- } catch {
- // not a parseable URL — no link
- }
- return undefined;
-}
-
export function buildMessage(
partial: Omit, "title"> & { title: string },
t: T,