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 { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
|
|
||||||
import { getExternalFaviconUrl, isExternalHttpUrl, isLoopbackHttpUrl, openExternalUrl } from '@/lib/url';
|
import { getExternalFaviconUrl, isExternalHttpUrl, isLoopbackHttpUrl, openExternalUrl } from '@/lib/url';
|
||||||
|
import {
|
||||||
|
buildAgentMentionUrl,
|
||||||
|
parseAgentHref,
|
||||||
|
parseSkillHref,
|
||||||
|
} from '@/lib/messages/inlineMessageLinks';
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
|
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
|
||||||
@@ -1000,6 +1005,38 @@ const buildMarkdownComponents = ({
|
|||||||
},
|
},
|
||||||
a({ href, children, ...props }) {
|
a({ href, children, ...props }) {
|
||||||
const targetHref = href ?? '';
|
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 isExternal = isExternalHttpUrl(targetHref);
|
||||||
const isLoopback = onPreviewLoopback ? isLoopbackHttpUrl(targetHref) : false;
|
const isLoopback = onPreviewLoopback ? isLoopbackHttpUrl(targetHref) : false;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import { useSkillsStore } from '@/stores/useSkillsStore';
|
|||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
import { getDirectoryForFilePath } from '@/lib/path-utils';
|
import { getDirectoryForFilePath } from '@/lib/path-utils';
|
||||||
|
import {
|
||||||
|
buildAgentHref,
|
||||||
|
buildAgentMentionUrl,
|
||||||
|
buildSkillHref,
|
||||||
|
parseSkillHref,
|
||||||
|
} from '@/lib/messages/inlineMessageLinks';
|
||||||
|
|
||||||
type PartWithText = Part & { text?: string; content?: string; value?: string };
|
type PartWithText = Part & { text?: string; content?: string; value?: string };
|
||||||
|
|
||||||
@@ -18,24 +24,7 @@ type UserTextPartProps = {
|
|||||||
agentMention?: AgentMentionInfo;
|
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_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 => {
|
const escapeHtml = (text: string): string => {
|
||||||
return text
|
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
|
// Step 1: First escape HTML to protect against XSS and ensure HTML tags display as text
|
||||||
content = escapeHtml(content);
|
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)) {
|
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>`;
|
const mentionMarkdown = `[${agentMention.token}](${buildAgentHref(agentMention.name)})`;
|
||||||
content = content.replace(agentMention.token, mentionHtml);
|
content = content.replace(agentMention.token, mentionMarkdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
content = content.replace(SKILL_TOKEN_PATTERN, (match, prefix: string, skillName: string) => {
|
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),
|
node.slice(0, idx),
|
||||||
<a
|
<a
|
||||||
key={`agent-${index}`}
|
key={`agent-${index}`}
|
||||||
href={buildMentionUrl(agentMention.name)}
|
href={buildAgentMentionUrl(agentMention.name)}
|
||||||
className="text-primary hover:underline"
|
className="text-primary hover:underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -1069,6 +1069,17 @@ html:not(.dark) .chat-scroll {
|
|||||||
color: var(--markdown-link-hover, var(--primary));
|
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 blockquote - use theme colors */
|
||||||
.markdown-content blockquote {
|
.markdown-content blockquote {
|
||||||
color: var(--markdown-blockquote, var(--muted-foreground));
|
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