+ ) : null}
+
{envelope.artifacts.length > 1 ? (
DISCORD_MESSAGE_MAX_LENGTH;
+}
+
+/**
+ * Builds a Discord warning when a markdown link is too long for a single message.
+ */
+export function getDiscordMarkdownLinkWarning(markdownLink: string): string | null {
+ if (!isDiscordMarkdownLinkTooLong(markdownLink)) {
+ return null;
+ }
+
+ const formattedLength = markdownLink.length.toLocaleString("en-US");
+ const limit = DISCORD_MESSAGE_MAX_LENGTH.toLocaleString("en-US");
+
+ return `This markdown link is ${formattedLength} characters, which exceeds Discord's ${limit} character message limit. Split the bundle into smaller artifacts and send separate markdown links in multiple Discord messages.`;
+}
+
+/**
+ * Formats a markdown link and returns share metadata, including a Discord warning when needed.
+ */
+export function buildMarkdownLinkShareInfo(label: string, href: string): MarkdownLinkShareInfo {
+ const markdownLink = formatMarkdownLink(label, href);
+
+ return {
+ markdownLink,
+ length: markdownLink.length,
+ discordWarning: getDiscordMarkdownLinkWarning(markdownLink),
+ };
+}
diff --git a/src/lib/payload/link-creator.ts b/src/lib/payload/link-creator.ts
index 9c5698d..d123a7f 100644
--- a/src/lib/payload/link-creator.ts
+++ b/src/lib/payload/link-creator.ts
@@ -1,5 +1,6 @@
import { normalizeEnvelope } from "@/lib/payload/envelope";
import { encodeEnvelope, encodeEnvelopeAsync, getVisibleFragmentLength } from "@/lib/payload/fragment";
+import { buildMarkdownLinkShareInfo } from "@/lib/markdown-link";
import {
codecForCompactTag,
codecs,
@@ -29,6 +30,9 @@ export type GeneratedArtifactLink = {
hash: string;
url: string;
fragmentLength: number;
+ markdownLink: string;
+ markdownLinkLength: number;
+ discordMarkdownLinkWarning: string | null;
};
const NON_WHITESPACE_PATTERN = /\S/;
@@ -114,6 +118,11 @@ function getFragmentCodec(fragmentBody: string): PayloadCodec {
return codecForCompactTag(fragmentBody.charAt(0)) ?? "plain";
}
+function buildGeneratedLinkShareInfo(envelope: PayloadEnvelope, url: string) {
+ const label = envelope.title ?? envelope.artifacts[0]?.title ?? envelope.artifacts[0]?.id ?? url;
+ return buildMarkdownLinkShareInfo(label, url);
+}
+
/**
* Builds a single-artifact payload envelope from link-creator draft input.
*
@@ -169,6 +178,8 @@ export function createGeneratedArtifactLink(draft: LinkCreatorDraft, baseUrl?: s
url = nextUrl.toString();
}
+ const shareInfo = buildGeneratedLinkShareInfo(normalized.envelope, url);
+
return {
envelope: normalized.envelope,
artifact: normalized.envelope.artifacts[0],
@@ -176,6 +187,9 @@ export function createGeneratedArtifactLink(draft: LinkCreatorDraft, baseUrl?: s
hash,
url,
fragmentLength,
+ markdownLink: shareInfo.markdownLink,
+ markdownLinkLength: shareInfo.length,
+ discordMarkdownLinkWarning: shareInfo.discordWarning,
};
}
@@ -212,6 +226,8 @@ export async function createGeneratedArtifactLinkAsync(draft: LinkCreatorDraft,
url = nextUrl.toString();
}
+ const shareInfo = buildGeneratedLinkShareInfo(normalized.envelope, url);
+
return {
envelope: normalized.envelope,
artifact: normalized.envelope.artifacts[0],
@@ -219,5 +235,8 @@ export async function createGeneratedArtifactLinkAsync(draft: LinkCreatorDraft,
hash,
url,
fragmentLength,
+ markdownLink: shareInfo.markdownLink,
+ markdownLinkLength: shareInfo.length,
+ discordMarkdownLinkWarning: shareInfo.discordWarning,
};
}
diff --git a/tests/components/link-creator.test.tsx b/tests/components/link-creator.test.tsx
index 4fc8d94..d28411d 100644
--- a/tests/components/link-creator.test.tsx
+++ b/tests/components/link-creator.test.tsx
@@ -2,6 +2,7 @@ import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { LinkCreator } from "@/components/home/link-creator";
+import { buildMarkdownLinkShareInfo } from "@/lib/markdown-link";
import type { GeneratedArtifactLink, LinkCreatorDraft } from "@/lib/payload/link-creator";
type PendingGeneration = {
@@ -23,6 +24,9 @@ vi.mock("@/lib/payload/link-creator", () => ({
}));
function createGeneratedLink(title: string): GeneratedArtifactLink {
+ const url = `https://agent-render.test/#agent-render=v1.plain.${title}`;
+ const shareInfo = buildMarkdownLinkShareInfo(title, url);
+
return {
artifact: {
id: title.toLowerCase().replace(/\s+/g, "-"),
@@ -41,7 +45,10 @@ function createGeneratedLink(title: string): GeneratedArtifactLink {
},
fragmentLength: 64,
hash: `#agent-render=v1.plain.${title}`,
- url: `https://agent-render.test/#agent-render=v1.plain.${title}`,
+ url,
+ markdownLink: shareInfo.markdownLink,
+ markdownLinkLength: shareInfo.length,
+ discordMarkdownLinkWarning: shareInfo.discordWarning,
};
}
diff --git a/tests/link-creator.test.ts b/tests/link-creator.test.ts
index 9915e89..86c3862 100644
--- a/tests/link-creator.test.ts
+++ b/tests/link-creator.test.ts
@@ -5,6 +5,7 @@ import arxDictionaryJson from "../public/arx-dictionary.json";
import { loadArx2OverlayDictionarySync, loadArxDictionarySync } from "@/lib/payload/arx-codec";
import { decodeFragment, decodeFragmentAsync } from "@/lib/payload/fragment";
import { createDraftEnvelope, createGeneratedArtifactLink, createGeneratedArtifactLinkAsync, type LinkCreatorDraft } from "@/lib/payload/link-creator";
+import { DISCORD_MESSAGE_MAX_LENGTH } from "@/lib/markdown-link";
import { compactTagForCodec } from "@/lib/payload/schema";
describe("link creator payloads", () => {
@@ -63,6 +64,9 @@ describe("link creator payloads", () => {
language: "tsx",
content: "export function ViewerShell() {\n return ;\n}",
});
+ expect(generatedLink.markdownLink).toContain("[Viewer shell]");
+ expect(generatedLink.markdownLinkLength).toBe(generatedLink.markdownLink.length);
+ expect(generatedLink.discordMarkdownLinkWarning).toBeNull();
});
it("keeps diff view settings in generated links", () => {
@@ -148,4 +152,26 @@ describe("link creator payloads", () => {
}),
).toThrow(/paste some content/i);
});
+
+ it("surfaces a Discord markdown link warning when the formatted link is too long", async () => {
+ const longContent = Array.from({ length: 2800 }, (_, index) =>
+ String.fromCharCode(33 + (index % 94)),
+ ).join("");
+ const generatedLink = await createGeneratedArtifactLinkAsync(
+ {
+ kind: "markdown",
+ title: "Long report",
+ filename: "long-report.md",
+ content: `# Report\n\n${longContent}`,
+ language: "",
+ diffView: "unified",
+ codec: "plain",
+ },
+ "https://agent-render.com/",
+ );
+
+ expect(generatedLink.markdownLinkLength).toBeGreaterThan(DISCORD_MESSAGE_MAX_LENGTH);
+ expect(generatedLink.discordMarkdownLinkWarning).toMatch(/Discord's 2,000 character message limit/i);
+ expect(generatedLink.discordMarkdownLinkWarning).toMatch(/multiple Discord messages/i);
+ });
});
diff --git a/tests/markdown-link.test.ts b/tests/markdown-link.test.ts
index 21b31c8..b7d9511 100644
--- a/tests/markdown-link.test.ts
+++ b/tests/markdown-link.test.ts
@@ -1,5 +1,11 @@
import { describe, expect, it } from "vitest";
-import { formatMarkdownLink } from "@/lib/markdown-link";
+import {
+ buildMarkdownLinkShareInfo,
+ DISCORD_MESSAGE_MAX_LENGTH,
+ formatMarkdownLink,
+ getDiscordMarkdownLinkWarning,
+ isDiscordMarkdownLinkTooLong,
+} from "@/lib/markdown-link";
describe("formatMarkdownLink", () => {
it("formats a standard inline link", () => {
@@ -27,4 +33,21 @@ describe("formatMarkdownLink", () => {
const href = "https://example.com/path?x=a)b>c";
expect(formatMarkdownLink("Wrapped", href)).toBe("[Wrapped]()");
});
+
+ it("flags markdown links that exceed Discord's message limit", () => {
+ const href = `https://example.com/#${"a".repeat(DISCORD_MESSAGE_MAX_LENGTH)}`;
+ const markdownLink = formatMarkdownLink("Report", href);
+
+ expect(isDiscordMarkdownLinkTooLong(markdownLink)).toBe(true);
+ expect(getDiscordMarkdownLinkWarning(markdownLink)).toMatch(/Discord's 2,000 character message limit/i);
+ expect(getDiscordMarkdownLinkWarning(markdownLink)).toMatch(/Split the bundle into smaller artifacts/i);
+ });
+
+ it("returns no Discord warning for links within the limit", () => {
+ const shareInfo = buildMarkdownLinkShareInfo("Weekly report", "https://agent-render.com/#pabc");
+
+ expect(shareInfo.length).toBeLessThanOrEqual(DISCORD_MESSAGE_MAX_LENGTH);
+ expect(shareInfo.discordWarning).toBeNull();
+ expect(shareInfo.markdownLink).toBe("[Weekly report](https://agent-render.com/#pabc)");
+ });
});
From 4c17ffe31db1f4ffc9039fc4b9a56c2338546559 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Sat, 20 Jun 2026 05:50:57 +0000
Subject: [PATCH 2/3] Document verbatim markdownLink for agents and add copy UI
Clarify in the agent skill that createGeneratedArtifactLink* returns a
ready-to-paste markdownLink string. Expose it in the link creator with a
dedicated copy action.
Co-authored-by: Aanish Bhirud
---
docs/payload-format.md | 2 +-
public/.well-known/agent-skills/index.json | 2 +-
skills/agent-render-linking/SKILL.md | 23 ++++++++--
src/components/home/link-creator.tsx | 49 ++++++++++++++++++++++
4 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/docs/payload-format.md b/docs/payload-format.md
index 13728a7..4faf0df 100644
--- a/docs/payload-format.md
+++ b/docs/payload-format.md
@@ -108,7 +108,7 @@ Tuple fields:
- Default sync codec priority is `deflate -> lz -> plain`
- Default async codec priority is `arx3 -> arx2 -> arx -> deflate -> lz -> plain`
- Optional budget-aware encoding can target strict limits and returns the shortest fragment when none fit
-- `createGeneratedArtifactLink` / `createGeneratedArtifactLinkAsync` return `markdownLink`, `markdownLinkLength`, and `discordMarkdownLinkWarning` so agents can detect Discord-unsafe markdown links before sharing
+- `createGeneratedArtifactLink` / `createGeneratedArtifactLinkAsync` return `url`, `markdownLink` (ready to paste verbatim in chat), `markdownLinkLength`, and `discordMarkdownLinkWarning` so agents do not need to reconstruct `[label](url)` themselves
When a payload does not fit the fragment budget or the target surface is hostile to long URLs, use UUID mode instead of weakening the fragment protocol. Current UUID mode stores the encoded payload server-side and is not zero-retention.
diff --git a/public/.well-known/agent-skills/index.json b/public/.well-known/agent-skills/index.json
index 6d74804..a0ae30e 100644
--- a/public/.well-known/agent-skills/index.json
+++ b/public/.well-known/agent-skills/index.json
@@ -6,7 +6,7 @@
"type": "skill-md",
"description": "Create zero-retention agent-render.com links for markdown, code, diffs, CSV, or JSON artifacts. Markdown artifacts support inline mermaid diagram rendering via fenced code blocks. Use when an agent needs to share a nicely rendered artifact in the browser instead of pasting raw content into chat. Trigger for requests like \"share this as a link\", \"make a diff link\", \"render this markdown/code/csv/json\", \"show this diagram\", or when chat rendering is weak. Agent Render is open source, hosted on Cloudflare Pages, and self-hostable. Use platform-specific linked-text syntax only on surfaces that support it cleanly, such as Discord Markdown links, Telegram HTML links, or Slack mrkdwn links; otherwise send a short summary plus the raw URL.",
"url": "https://raw.githubusercontent.com/baanish/agent-render/main/skills/agent-render-linking/SKILL.md",
- "digest": "sha256:1472351a9393725a922c453952f2ddffc227e1f1c1273d0b582ee1f2c484e978"
+ "digest": "sha256:93826ebd4143754c284e5e6445de33bc2978e01921fd9ef8ce13c5fef028ad29"
},
{
"name": "selfhosted-agent-render",
diff --git a/skills/agent-render-linking/SKILL.md b/skills/agent-render-linking/SKILL.md
index 0b06ce4..47912b9 100644
--- a/skills/agent-render-linking/SKILL.md
+++ b/skills/agent-render-linking/SKILL.md
@@ -173,6 +173,20 @@ Example cases:
Set `activeArtifactId` to the artifact that should open first.
+## Ready-to-send markdown link
+
+Do not hand-assemble `[label](url)` when the product already formatted it for you.
+
+When using the link helpers (`createGeneratedArtifactLink` / `createGeneratedArtifactLinkAsync`), use the returned `markdownLink` string verbatim in chat. It is the exact Discord/Slack-style markdown link the viewer would copy, including label escaping and URL wrapping for special characters.
+
+The result also includes:
+- `markdownLinkLength` — total character count of `markdownLink`
+- `discordMarkdownLinkWarning` — non-null when `markdownLink` exceeds Discord's 2,000 character message limit
+
+If you built the URL yourself and only need the formatted link, call `buildMarkdownLinkShareInfo(label, url)` and send `markdownLink` verbatim. Check `discordWarning` before posting to Discord.
+
+Only fall back to manual `formatMarkdownLink(label, url)` when you cannot use the helpers above.
+
## Link construction
Construct the final URL with the compact `#` fragment:
@@ -257,9 +271,9 @@ Respect these limits:
- target decoded payload budget: about 200,000 characters
- Discord message limit for a single markdown link: 2,000 characters total for the formatted `[label](url)` string
-Before sharing on Discord, format the link with `formatMarkdownLink(label, url)` (or the equivalent in your language) and check the total character count. If it exceeds 2,000 characters, the message will probably break on Discord. Split the bundle into smaller artifacts and send separate markdown links in multiple Discord messages instead of one oversized link.
+Before sharing on Discord, check `markdownLinkLength` or `discordMarkdownLinkWarning` from the link helpers. If you formatted the link yourself, use `buildMarkdownLinkShareInfo(label, url)` and inspect `discordWarning`. When the warning is non-null, split the bundle into smaller artifacts and send separate markdown links in multiple Discord messages instead of one oversized link.
-When generating links programmatically via `createGeneratedArtifactLink` / `createGeneratedArtifactLinkAsync`, inspect `discordMarkdownLinkWarning` on the result. When it is non-null, surface that warning to the caller and split the payload before sharing on Discord.
+When generating links programmatically via `createGeneratedArtifactLink` / `createGeneratedArtifactLinkAsync`, send `markdownLink` verbatim and inspect `discordMarkdownLinkWarning`. When it is non-null, surface that warning to the caller and split the payload before sharing on Discord.
If a link is getting too large:
1. try `arx3` first for trusted Unicode-preserving surfaces; otherwise try `arx2`, then `arx`, then `deflate`, then `lz`, then `plain`
@@ -284,13 +298,13 @@ Use platform-specific link text only on surfaces that support it cleanly.
### Discord
-Prefer standard Markdown links:
+Prefer standard Markdown links. When you have `markdownLink` from the link helpers, paste that string verbatim:
```md
[Short summary](https://agent-render.com/#)
```
-Check the total formatted markdown link length before sending. Discord rejects messages longer than 2,000 characters, so a single `[label](url)` string that exceeds that limit will probably fail. When it does, split the artifact into smaller bundles and send multiple markdown links across separate Discord messages.
+Check `markdownLinkLength` or `discordMarkdownLinkWarning` before sending. Discord rejects messages longer than 2,000 characters, so a single `[label](url)` string that exceeds that limit will probably fail. When it does, split the artifact into smaller bundles and send multiple markdown links across separate Discord messages.
Examples:
- `[Weekly report](https://agent-render.com/#)`
@@ -337,6 +351,7 @@ When sharing a link:
- Prefer `patch` for diffs
- Prefer readable titles
- Prefer Markdown link text when supported
+- Send `markdownLink` verbatim instead of reconstructing `[label](url)` yourself
- Check `discordMarkdownLinkWarning` before sharing markdown links on Discord
- Prefer shortest-by-measurement instead of human guesses
- Use budget-aware encoding for Discord-like constraints
diff --git a/src/components/home/link-creator.tsx b/src/components/home/link-creator.tsx
index ad4d94d..4d7aa38 100644
--- a/src/components/home/link-creator.tsx
+++ b/src/components/home/link-creator.tsx
@@ -95,6 +95,9 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">(
"idle",
);
+ const [markdownLinkCopyState, setMarkdownLinkCopyState] = useState<
+ "idle" | "copied" | "failed"
+ >("idle");
const generationRequestRef = useRef(0);
const isGeneratedLinkStale =
Boolean(generatedLink) && draftVersion !== generatedVersion;
@@ -104,6 +107,7 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
useEffect(() => {
setCopyState("idle");
+ setMarkdownLinkCopyState("idle");
setError(null);
}, [draftVersion]);
@@ -145,6 +149,7 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
setGeneratedVersion(draftVersion);
setError(null);
setCopyState("idle");
+ setMarkdownLinkCopyState("idle");
} catch (generationError) {
if (generationRequestRef.current !== requestId) {
return;
@@ -153,6 +158,7 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
setGeneratedLink(null);
setGeneratedVersion(-1);
setCopyState("idle");
+ setMarkdownLinkCopyState("idle");
setError(
generationError instanceof Error
? generationError.message
@@ -174,6 +180,19 @@ export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
}
};
+ const handleCopyMarkdownLink = async () => {
+ if (!generatedLink) {
+ return;
+ }
+
+ try {
+ await copyTextToClipboard(generatedLink.markdownLink);
+ setMarkdownLinkCopyState("copied");
+ } catch {
+ setMarkdownLinkCopyState("failed");
+ }
+ };
+
return (
+
) : null}
diff --git a/src/lib/markdown-link.ts b/src/lib/markdown-link.ts
index b3eaaa0..d6f86c9 100644
--- a/src/lib/markdown-link.ts
+++ b/src/lib/markdown-link.ts
@@ -1,6 +1,8 @@
/** Discord's per-message character limit. */
export const DISCORD_MESSAGE_MAX_LENGTH = 2000;
+const discordNumberFormatter = new Intl.NumberFormat("en-US");
+
/**
* Escape characters that would break a markdown inline link label.
*/
@@ -43,19 +45,33 @@ export function isDiscordMarkdownLinkTooLong(markdownLink: string): boolean {
}
/**
- * Builds a Discord warning when a markdown link is too long for a single message.
+ * Builds an agent-facing Discord warning when a markdown link is too long for a single message.
*/
export function getDiscordMarkdownLinkWarning(markdownLink: string): string | null {
if (!isDiscordMarkdownLinkTooLong(markdownLink)) {
return null;
}
- const formattedLength = markdownLink.length.toLocaleString("en-US");
- const limit = DISCORD_MESSAGE_MAX_LENGTH.toLocaleString("en-US");
+ const formattedLength = discordNumberFormatter.format(markdownLink.length);
+ const limit = discordNumberFormatter.format(DISCORD_MESSAGE_MAX_LENGTH);
return `This markdown link is ${formattedLength} characters, which exceeds Discord's ${limit} character message limit. Split the bundle into smaller artifacts and send separate markdown links in multiple Discord messages.`;
}
+/**
+ * Builds a viewer-facing notice when a markdown link may be too long to post in Discord.
+ */
+export function getDiscordMarkdownLinkViewerNotice(markdownLink: string): string | null {
+ if (!isDiscordMarkdownLinkTooLong(markdownLink)) {
+ return null;
+ }
+
+ const formattedLength = discordNumberFormatter.format(markdownLink.length);
+ const limit = discordNumberFormatter.format(DISCORD_MESSAGE_MAX_LENGTH);
+
+ return `This markdown link is ${formattedLength} characters, which may be too long to post directly in Discord's ${limit} character message limit.`;
+}
+
/**
* Formats a markdown link and returns share metadata, including a Discord warning when needed.
*/
diff --git a/tests/markdown-link.test.ts b/tests/markdown-link.test.ts
index b7d9511..b9c0cd8 100644
--- a/tests/markdown-link.test.ts
+++ b/tests/markdown-link.test.ts
@@ -4,6 +4,7 @@ import {
DISCORD_MESSAGE_MAX_LENGTH,
formatMarkdownLink,
getDiscordMarkdownLinkWarning,
+ getDiscordMarkdownLinkViewerNotice,
isDiscordMarkdownLinkTooLong,
} from "@/lib/markdown-link";
@@ -50,4 +51,13 @@ describe("formatMarkdownLink", () => {
expect(shareInfo.discordWarning).toBeNull();
expect(shareInfo.markdownLink).toBe("[Weekly report](https://agent-render.com/#pabc)");
});
+
+ it("uses viewer-facing notice copy without split guidance", () => {
+ const href = `https://example.com/#${"a".repeat(DISCORD_MESSAGE_MAX_LENGTH)}`;
+ const markdownLink = formatMarkdownLink("Report", href);
+ const notice = getDiscordMarkdownLinkViewerNotice(markdownLink);
+
+ expect(notice).toMatch(/may be too long to post directly in Discord/i);
+ expect(notice).not.toMatch(/Split the bundle/i);
+ });
});