fix: style agent mentions with primary color in markdown messages
Agent @mentions in markdown use the primary accent instead of generic link color Mentions render without external URL favicons Markdown mode now matches plain text mention styling
This commit is contained in:
@@ -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 (
|
||||
<a
|
||||
{...props}
|
||||
href={buildAgentMentionUrl(agentName)}
|
||||
data-openchamber-agent-mention="true"
|
||||
className={cn('text-primary hover:underline', props.className)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const skillName = parseSkillHref(targetHref);
|
||||
if (skillName) {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={targetHref}
|
||||
data-skill-name={skillName}
|
||||
className={cn('text-primary hover:underline', props.className)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const isExternal = isExternalHttpUrl(targetHref);
|
||||
const isLoopback = onPreviewLoopback ? isLoopbackHttpUrl(targetHref) : false;
|
||||
return (
|
||||
|
||||
@@ -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<UserTextPartProps> = ({ 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 <a> 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 = `<a href="${buildMentionUrl(agentMention.name)}" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">${agentMention.token}</a>`;
|
||||
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<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
node.slice(0, idx),
|
||||
<a
|
||||
key={`agent-${index}`}
|
||||
href={buildMentionUrl(agentMention.name)}
|
||||
href={buildAgentMentionUrl(agentMention.name)}
|
||||
className="text-primary hover:underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -1069,6 +1069,17 @@ html:not(.dark) .chat-scroll {
|
||||
color: var(--markdown-link-hover, var(--primary));
|
||||
}
|
||||
|
||||
/* Agent/skill mentions use the primary accent, not the markdown link color */
|
||||
.markdown-content a[data-openchamber-agent-mention="true"],
|
||||
.markdown-content a[data-skill-name] {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.markdown-content a[data-openchamber-agent-mention="true"]:hover,
|
||||
.markdown-content a[data-skill-name]:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Markdown blockquote - use theme colors */
|
||||
.markdown-content blockquote {
|
||||
color: var(--markdown-blockquote, var(--muted-foreground));
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export const SKILL_LINK_PREFIX = '#openchamber-skill:';
|
||||
export const AGENT_LINK_PREFIX = '#openchamber-agent:';
|
||||
|
||||
export const buildAgentMentionUrl = (name: string): string => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user