add @mentions — parsed server-side, rendered as highlighted spans

- Backend: parse_mentions() extracts @username from content
- Message API response includes mentions: string[] field
- Frontend: renderContent() highlights @mentions in hot orange
- Works with alphanumeric + hyphens + underscores

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 20:56:45 +02:00
parent 6ab217970e
commit 8ce8ebc9b8
3 changed files with 28 additions and 1 deletions

View File

@@ -44,6 +44,7 @@ pub struct Message {
pub content: String,
#[ts(optional)]
pub metadata: Option<serde_json::Value>,
pub mentions: Vec<String>,
#[ts(optional)]
pub reply_to: Option<Uuid>,
pub created_at: DateTime<Utc>,

View File

@@ -2,6 +2,16 @@ use chrono::{DateTime, Utc};
use sqlx::FromRow;
use uuid::Uuid;
/// Extract @mentions from message content
fn parse_mentions(content: &str) -> Vec<String> {
content
.split_whitespace()
.filter(|w| w.starts_with('@') && w.len() > 1)
.map(|w| w.trim_start_matches('@').trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '-').to_string())
.filter(|m| !m.is_empty())
.collect()
}
/// DB row types — these map directly to SQL tables.
/// Separate from API types in colony-types.
@@ -87,6 +97,7 @@ impl MessageRow {
} else {
self.content.clone()
},
mentions: parse_mentions(&self.content),
metadata: self
.metadata
.as_ref()

View File

@@ -26,6 +26,21 @@ function timeAgo(dateStr: string): string {
return `${Math.floor(hrs / 24)}d`;
}
function renderContent(text: string) {
// Split on @mentions and render them as highlighted spans
const parts = text.split(/(@[\w-]+)/g);
return parts.map((part, i) => {
if (part.startsWith("@")) {
return (
<span key={i} className="text-primary font-bold cursor-default">
{part}
</span>
);
}
return part;
});
}
export function MessageItem({ message, replyTarget, onReply }: Props) {
const [metaOpen, setMetaOpen] = useState(false);
const isAgent = message.user.role === "agent";
@@ -110,7 +125,7 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
{isDeleted ? (
<span className="italic text-muted-foreground/40">[deleted]</span>
) : (
message.content
renderContent(message.content)
)}
</div>