diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
index efd7d0b1..e1390129 100644
--- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
+++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx
@@ -19,6 +19,11 @@ import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { getExternalFaviconUrl, isExternalHttpUrl, isLoopbackHttpUrl, openExternalUrl } from '@/lib/url';
+import {
+ buildAgentMentionUrl,
+ parseAgentHref,
+ parseSkillHref,
+} from '@/lib/messages/inlineMessageLinks';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { getDefaultTheme } from '@/lib/theme/themes';
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
@@ -1000,6 +1005,38 @@ const buildMarkdownComponents = ({
},
a({ href, children, ...props }) {
const targetHref = href ?? '';
+ const agentName = parseAgentHref(targetHref);
+ if (agentName) {
+ return (
+ event.stopPropagation()}
+ >
+ {children}
+
+ );
+ }
+
+ const skillName = parseSkillHref(targetHref);
+ if (skillName) {
+ return (
+ event.stopPropagation()}
+ >
+ {children}
+
+ );
+ }
+
const isExternal = isExternalHttpUrl(targetHref);
const isLoopback = onPreviewLoopback ? isLoopbackHttpUrl(targetHref) : false;
return (
diff --git a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx
index 205c2780..b7955325 100644
--- a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx
+++ b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx
@@ -8,6 +8,12 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
import { Icon } from "@/components/icon/Icon";
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { getDirectoryForFilePath } from '@/lib/path-utils';
+import {
+ buildAgentHref,
+ buildAgentMentionUrl,
+ buildSkillHref,
+ parseSkillHref,
+} from '@/lib/messages/inlineMessageLinks';
type PartWithText = Part & { text?: string; content?: string; value?: string };
@@ -18,24 +24,7 @@ type UserTextPartProps = {
agentMention?: AgentMentionInfo;
};
-const buildMentionUrl = (name: string): string => {
- const encoded = encodeURIComponent(name);
- return `https://opencode.ai/docs/agents/#${encoded}`;
-};
-
const SKILL_TOKEN_PATTERN = /(^|\s)\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)/g;
-const SKILL_LINK_PREFIX = '#openchamber-skill:';
-
-const buildSkillHref = (name: string): string => `${SKILL_LINK_PREFIX}${encodeURIComponent(name)}`;
-
-const parseSkillHref = (href: string | null | undefined): string | null => {
- if (!href?.startsWith(SKILL_LINK_PREFIX)) return null;
- try {
- return decodeURIComponent(href.slice(SKILL_LINK_PREFIX.length));
- } catch {
- return null;
- }
-};
const escapeHtml = (text: string): string => {
return text
@@ -150,10 +139,10 @@ const UserTextPart: React.FC = ({ part, messageId, agentMenti
// Step 1: First escape HTML to protect against XSS and ensure HTML tags display as text
content = escapeHtml(content);
- // Step 2: Then insert agent mention links (after escaping, so tags won't be escaped)
+ // Step 2: Insert agent mention links with an internal href so markdown renders them as mentions, not external links.
if (agentMention?.token && content.includes(agentMention.token)) {
- const mentionHtml = `${agentMention.token}`;
- content = content.replace(agentMention.token, mentionHtml);
+ const mentionMarkdown = `[${agentMention.token}](${buildAgentHref(agentMention.name)})`;
+ content = content.replace(agentMention.token, mentionMarkdown);
}
content = content.replace(SKILL_TOKEN_PATTERN, (match, prefix: string, skillName: string) => {
@@ -214,7 +203,7 @@ const UserTextPart: React.FC = ({ part, messageId, agentMenti
node.slice(0, idx),
{
+ const encoded = encodeURIComponent(name);
+ return `https://opencode.ai/docs/agents/#${encoded}`;
+};
+
+export const buildSkillHref = (name: string): string => `${SKILL_LINK_PREFIX}${encodeURIComponent(name)}`;
+
+export const buildAgentHref = (name: string): string => `${AGENT_LINK_PREFIX}${encodeURIComponent(name)}`;
+
+export const parseSkillHref = (href: string | null | undefined): string | null => {
+ if (!href?.startsWith(SKILL_LINK_PREFIX)) return null;
+ try {
+ return decodeURIComponent(href.slice(SKILL_LINK_PREFIX.length));
+ } catch {
+ return null;
+ }
+};
+
+export const parseAgentHref = (href: string | null | undefined): string | null => {
+ if (!href?.startsWith(AGENT_LINK_PREFIX)) return null;
+ try {
+ return decodeURIComponent(href.slice(AGENT_LINK_PREFIX.length));
+ } catch {
+ return null;
+ }
+};