From 26682973e7bc79b6d46b7e0bef17d7ded4387925 Mon Sep 17 00:00:00 2001
From: ZhenYi <434836402@qq.com>
Date: Fri, 17 Apr 2026 23:43:26 +0800
Subject: [PATCH] feat(room): redesign mention system with AST-based format
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Backend:
- Add MENTION_TAG_RE matching new `label` format
- Extend extract_mentions() and resolve_mentions() to parse new format (legacy backward-compatible)
Frontend:
- New src/lib/mention-ast.ts: AST types (TextNode, MentionNode, AiActionNode),
parse() and serialize() functions for AST↔HTML conversion
- MentionPopover: load @repository: from project repos, @ai: from room_ai configs
(not room members); output new HTML format with ID instead of label
- MessageMentions: use AST parse() for rendering (falls back to legacy parser)
- ChatInputArea: insertMention now produces `label`
- RoomParticipantsPanel: onMention passes member UUID to insertMention
- RoomContext: add projectRepos and roomAiConfigs for mention data sources
---
libs/room/src/service.rs | 70 ++++++++
src/components/room/MentionPopover.tsx | 88 +++++++---
src/components/room/MessageMentions.tsx | 101 ++++++-----
src/components/room/RoomChatPanel.tsx | 26 ++-
src/components/room/RoomParticipantsPanel.tsx | 6 +-
src/components/room/RoomSettingsPanel.tsx | 3 +-
src/contexts/room-context.tsx | 83 +++++++++
src/lib/mention-ast.ts | 165 ++++++++++++++++++
8 files changed, 461 insertions(+), 81 deletions(-)
create mode 100644 src/lib/mention-ast.ts
diff --git a/libs/room/src/service.rs b/libs/room/src/service.rs
index bb775f7..e39df2d 100644
--- a/libs/room/src/service.rs
+++ b/libs/room/src/service.rs
@@ -27,6 +27,15 @@ const DEFAULT_MAX_CONCURRENT_WORKERS: usize = 1024;
static USER_MENTION_RE: LazyLock regex_lite::Regex> =
LazyLock::new(|| regex_lite::Regex::new(r"\s*([^<]+?)\s*").unwrap());
+/// Matches label
+static MENTION_TAG_RE: LazyLock regex_lite::Regex> =
+ LazyLock::new(|| {
+ regex_lite::Regex::new(
+ r#"]*>\s*([^<]*?)\s*"#,
+ )
+ .unwrap()
+ });
+
#[derive(Clone)]
pub struct RoomService {
pub db: AppDatabase,
@@ -533,8 +542,12 @@ impl RoomService {
Ok(())
}
+ /// Extracts user UUIDs from both the legacy `uuid` format
+ /// and the new `label` format.
pub fn extract_mentions(content: &str) -> Vec {
let mut mentioned = Vec::new();
+
+ // Legacy uuid format
for cap in USER_MENTION_RE.captures_iter(content) {
if let Some(inner) = cap.get(1) {
let token = inner.as_str().trim();
@@ -546,9 +559,25 @@ impl RoomService {
}
}
+ // New label format
+ for cap in MENTION_TAG_RE.captures_iter(content) {
+ if let (Some(type_m), Some(id_m)) = (cap.get(1), cap.get(2)) {
+ if type_m.as_str() == "user" {
+ if let Ok(uuid) = Uuid::parse_str(id_m.as_str().trim()) {
+ if !mentioned.contains(&uuid) {
+ mentioned.push(uuid);
+ }
+ }
+ }
+ }
+ }
+
mentioned
}
+ /// Resolves user mentions from both the legacy `...` format and the
+ /// new `label` format.
+ /// Repository and AI mention types are accepted but produce no user UUIDs.
pub async fn resolve_mentions(&self, content: &str) -> Vec {
use models::users::User;
use sea_orm::EntityTrait;
@@ -556,6 +585,7 @@ impl RoomService {
let mut resolved: Vec = Vec::new();
let mut seen_usernames: Vec = Vec::new();
+ // Legacy uuid or username format
for cap in USER_MENTION_RE.captures_iter(content) {
if let Some(inner) = cap.get(1) {
let token = inner.as_str().trim();
@@ -587,6 +617,46 @@ impl RoomService {
}
}
+ // New label format
+ for cap in MENTION_TAG_RE.captures_iter(content) {
+ if let (Some(type_m), Some(id_m)) = (cap.get(1), cap.get(2)) {
+ if type_m.as_str() == "user" {
+ let id = id_m.as_str().trim();
+ if let Ok(uuid) = Uuid::parse_str(id) {
+ if !resolved.contains(&uuid) {
+ resolved.push(uuid);
+ }
+ } else {
+ // Fall back to label-based username lookup
+ if let Some(label_m) = cap.get(3) {
+ let label = label_m.as_str().trim();
+ if !label.is_empty() {
+ let label_lower = label.to_lowercase();
+ if seen_usernames.contains(&label_lower) {
+ continue;
+ }
+ seen_usernames.push(label_lower.clone());
+
+ if let Some(user) = User::find()
+ .filter(models::users::user::Column::Username.eq(label_lower))
+ .one(&self.db)
+ .await
+ .ok()
+ .flatten()
+ {
+ if !resolved.contains(&user.uid) {
+ resolved.push(user.uid);
+ }
+ }
+ }
+ }
+ }
+ }
+ // `repository` and `ai` mention types are accepted but do not
+ // produce user notification UUIDs — no-op here.
+ }
+ }
+
resolved
}
diff --git a/src/components/room/MentionPopover.tsx b/src/components/room/MentionPopover.tsx
index b9f0873..d0ccf9c 100644
--- a/src/components/room/MentionPopover.tsx
+++ b/src/components/room/MentionPopover.tsx
@@ -1,15 +1,25 @@
-import type { RoomMemberResponse } from '@/client';
+import type { ProjectRepositoryItem, RoomMemberResponse } from '@/client';
+import { buildMentionHtml, type MentionMentionType } from '@/lib/mention-ast';
import { cn } from '@/lib/utils';
-import { Check, CornerDownLeft, Keyboard, SearchX } from 'lucide-react';
+import { Bot, Check, CornerDownLeft, Keyboard, SearchX } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { SetStateAction } from 'react';
import { ModelIcon } from './icon-match';
+/** Room AI config — the configured model for this room */
+export interface RoomAiConfig {
+ model: string; // model UID / model ID
+ modelName?: string;
+}
+
interface MentionSuggestion {
type: 'category' | 'item';
- category?: 'repository' | 'user' | 'ai';
+ category?: MentionMentionType;
label: string;
+ /** Raw value stored in suggestion (for display) */
value: string;
+ /** ID used when building HTML mention (absent for category headers) */
+ mentionId?: string;
avatar?: string | null;
}
@@ -27,10 +37,12 @@ interface PopoverPosition {
}
interface MentionPopoverProps {
- /** Available members for user/ai mention suggestions */
+ /** Available members for @user: mention suggestions */
members: RoomMemberResponse[];
- /** Repository names available for @repository: mention */
- repos?: string[];
+ /** Available repositories for @repository: mention suggestions */
+ repos?: ProjectRepositoryItem[];
+ /** Room AI configs for @ai: mention suggestions */
+ aiConfigs?: RoomAiConfig[];
inputValue: string;
cursorPosition: number;
onSelect: (newValue: string, newCursorPosition: number) => void;
@@ -41,6 +53,7 @@ interface MentionPopoverProps {
export function MentionPopover({
members,
repos = [],
+ aiConfigs = [],
inputValue,
cursorPosition,
onSelect,
@@ -109,12 +122,13 @@ export function MentionPopover({
if (category === 'repository') {
return repos
- .filter((repo) => !item || repo.toLowerCase().includes(item))
+ .filter((repo) => !item || repo.repo_name.toLowerCase().includes(item))
.map((repo) => ({
type: 'item' as const,
category: 'repository' as const,
- label: repo,
- value: `@repository:${repo}`,
+ label: repo.repo_name,
+ value: `@repository:${repo.repo_name}`,
+ mentionId: repo.uid,
avatar: null,
}));
}
@@ -132,33 +146,34 @@ export function MentionPopover({
type: 'item' as const,
category: 'user' as const,
label: username,
- value: `@user:${m.user}`,
+ value: `@user:${username}`,
+ mentionId: m.user,
avatar: m.user_info?.avatar_url ?? null,
};
});
}
if (category === 'ai') {
- return members
- .filter((m) => m.role === 'ai')
- .filter((m) => {
- const username = m.user_info?.username ?? m.user;
- return !item || username.toLowerCase().includes(item);
+ return aiConfigs
+ .filter((cfg) => {
+ const name = cfg.modelName ?? cfg.model;
+ return !item || name.toLowerCase().includes(item);
})
- .map((m) => {
- const username = m.user_info?.username ?? m.user;
+ .map((cfg) => {
+ const label = cfg.modelName ?? cfg.model;
return {
type: 'item' as const,
category: 'ai' as const,
- label: username,
- value: `@ai:${m.user}`,
- avatar: m.user_info?.avatar_url ?? null,
+ label,
+ value: `@ai:${label}`,
+ mentionId: cfg.model,
+ avatar: null,
};
});
}
return [];
- }, [mentionState, members, repos]);
+ }, [mentionState, members, repos, aiConfigs]);
const visibleSuggestions = useMemo(() => suggestions.slice(0, 8), [suggestions]);
const validSelectedIndex = selectedIndex >= visibleSuggestions.length ? 0 : selectedIndex;
@@ -173,16 +188,27 @@ export function MentionPopover({
const before = inputValue.slice(0, mentionState.startPos);
const after = inputValue.slice(cursorPosition);
- const spacer = suggestion.type === 'item' ? ' ' : '';
- const newValue = before + suggestion.value + spacer + after;
- const newCursorPos = mentionState.startPos + suggestion.value.length + spacer.length;
- onSelect(newValue, newCursorPos);
+ let newValue: string;
+ let newCursorPos: number;
- if (suggestion.type === 'item') {
- closePopover();
- } else {
+ if (suggestion.type === 'category') {
+ // User selected a category header (e.g., @user:) — keep the partial
+ newValue = before + suggestion.value + after;
+ newCursorPos = mentionState.startPos + suggestion.value.length;
onOpenChange(true);
+ } else {
+ // Build new HTML mention string
+ const html = buildMentionHtml(
+ suggestion.category!,
+ suggestion.mentionId!,
+ suggestion.label,
+ );
+ const spacer = ' ';
+ newValue = before + html + spacer + after;
+ newCursorPos = mentionState.startPos + html.length + spacer.length;
+ onSelect(newValue, newCursorPos);
+ closePopover();
}
};
}, [mentionState, inputValue, cursorPosition, onSelect, closePopover, onOpenChange]);
@@ -351,7 +377,11 @@ export function MentionPopover({
{suggestion.type === 'category' ? (
'>'
) : suggestion.category === 'ai' ? (
-
+ suggestion.avatar ? (
+
+ ) : (
+
+ )
) : (
suggestion.label[0]?.toUpperCase()
)}
diff --git a/src/components/room/MessageMentions.tsx b/src/components/room/MessageMentions.tsx
index 84d32ca..4ef0950 100644
--- a/src/components/room/MessageMentions.tsx
+++ b/src/components/room/MessageMentions.tsx
@@ -1,10 +1,10 @@
import { memo, useMemo } from 'react';
import { cn } from '@/lib/utils';
-
+import { parse, type Node, type MentionMentionType } from '@/lib/mention-ast';
type MentionType = 'repository' | 'user' | 'ai' | 'notify';
-interface MentionToken {
+interface LegacyMentionToken {
full: string;
type: MentionType;
name: string;
@@ -14,12 +14,17 @@ function isMentionNameChar(ch: string): boolean {
return /[A-Za-z0-9._:\/-]/.test(ch);
}
-function extractMentionTokens(text: string): MentionToken[] {
- const tokens: MentionToken[] = [];
+/**
+ * Parses legacy mention formats for backward compatibility with old messages:
+ * - `name` (old backend format)
+ * - `@type:name` (old colon format)
+ * - `name` (old XML format)
+ */
+function extractLegacyMentionTokens(text: string): LegacyMentionToken[] {
+ const tokens: LegacyMentionToken[] = [];
let cursor = 0;
while (cursor < text.length) {
- // Find next potential mention: or or @type:name
const colon = text.indexOf(':', typeStart);
const validTypes: MentionType[] = ['repository', 'user', 'ai', 'notify'];
if (colon >= 0 && colon - typeStart <= 20) {
- // Colon format: or @type:name
const typeRaw = text.slice(typeStart, colon).toLowerCase();
if (!validTypes.includes(typeRaw as MentionType)) {
cursor = next + 1;
@@ -54,7 +56,6 @@ function extractMentionTokens(text: string): MentionToken[] {
end++;
}
- // For angle style, require closing >
const closeBracket = style === 'angle' && text[end] === '>' ? end + 1 : end;
const name = text.slice(colon + 1, style === 'angle' ? end : closeBracket);
@@ -65,10 +66,7 @@ function extractMentionTokens(text: string): MentionToken[] {
}
}
- // Check for XML-like format: name
- // Only for angle style
if (style === 'angle') {
- // Find space or > to determine type end
let typeEnd = typeStart;
while (typeEnd < text.length && /[A-Za-z]/.test(text[typeEnd])) {
typeEnd++;
@@ -76,11 +74,8 @@ function extractMentionTokens(text: string): MentionToken[] {
const typeCandidate = text.slice(typeStart, typeEnd);
if (validTypes.includes(typeCandidate as MentionType)) {
- // Found opening tag like
const closeTag = `${typeCandidate}>`;
const contentStart = typeEnd;
-
- // Find the closing tag
const tagClose = text.indexOf(closeTag, cursor);
if (tagClose >= 0) {
const name = text.slice(contentStart, tagClose);
@@ -99,7 +94,7 @@ function extractMentionTokens(text: string): MentionToken[] {
}
function extractFirstMentionName(text: string, type: MentionType): string | null {
- const token = extractMentionTokens(text).find((item) => item.type === type);
+ const token = extractLegacyMentionTokens(text).find((item) => item.type === type);
return token?.name ?? null;
}
@@ -107,38 +102,73 @@ interface MessageContentWithMentionsProps {
content: string;
}
-const mentionStyles: Record = {
+const mentionStyles: Record = {
user: 'inline-flex items-center rounded bg-blue-100/80 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 font-medium cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900/60 transition-colors text-sm leading-5',
repository: 'inline-flex items-center rounded bg-purple-100/80 px-1.5 py-0.5 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-medium cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-900/60 transition-colors text-sm leading-5',
ai: 'inline-flex items-center rounded bg-green-100/80 px-1.5 py-0.5 text-green-700 dark:bg-green-900/40 dark:text-green-300 font-medium cursor-pointer hover:bg-green-200 dark:hover:bg-green-900/60 transition-colors text-sm leading-5',
notify: 'inline-flex items-center rounded bg-yellow-100/80 px-1.5 py-0.5 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300 font-medium cursor-pointer hover:bg-yellow-200 dark:hover:bg-yellow-900/60 transition-colors text-sm leading-5',
};
-/** Renders message content with @mention highlighting using styled spans */
+function renderNode(node: Node, index: number): React.ReactNode {
+ if (node.type === 'text') {
+ return {node.text};
+ }
+ if (node.type === 'mention') {
+ return (
+
+ @{node.label}
+
+ );
+ }
+ if (node.type === 'ai_action') {
+ return (
+
+ /{node.action}
+ {node.args ? ` ${node.args}` : ''}
+
+ );
+ }
+ return null;
+}
+
+/** Renders message content with @mention highlighting using styled spans.
+ * Supports the new `label` format
+ * and legacy formats for backward compatibility with old messages.
+ */
export const MessageContentWithMentions = memo(function MessageContentWithMentions({
content,
}: MessageContentWithMentionsProps) {
- const processed = useMemo(() => {
- const tokens = extractMentionTokens(content);
- if (tokens.length === 0) return [{ type: 'text' as const, content }];
+ const nodes = useMemo(() => {
+ // Try the new AST parser first (handles and tags)
+ const ast = parse(content);
+ if (ast.length > 0) return ast;
- const parts: Array<{ type: 'text'; content: string } | { type: 'mention'; mention: MentionToken }> = [];
+ // Fall back to legacy parser for old-format messages
+ const legacy = extractLegacyMentionTokens(content);
+ if (legacy.length === 0) return [{ type: 'text' as const, text: content }];
+
+ const parts: Node[] = [];
let cursor = 0;
-
- for (const token of tokens) {
+ for (const token of legacy) {
const idx = content.indexOf(token.full, cursor);
if (idx === -1) continue;
if (idx > cursor) {
- parts.push({ type: 'text', content: content.slice(cursor, idx) });
+ parts.push({ type: 'text', text: content.slice(cursor, idx) });
}
- parts.push({ type: 'mention', mention: token });
+ parts.push({
+ type: 'mention',
+ mentionType: token.type as MentionMentionType,
+ id: '',
+ label: token.name,
+ });
cursor = idx + token.full.length;
}
-
if (cursor < content.length) {
- parts.push({ type: 'text', content: content.slice(cursor) });
+ parts.push({ type: 'text', text: content.slice(cursor) });
}
-
return parts;
}, [content]);
@@ -151,18 +181,7 @@ export const MessageContentWithMentions = memo(function MessageContentWithMentio
'[&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:overflow-x-auto',
)}
>
- {processed.map((part, i) =>
- part.type === 'mention' ? (
-
- @{part.mention.name}
-
- ) : (
- {part.content}
- ),
- )}
+ {nodes.map((node, i) => renderNode(node, i))}
);
});
diff --git a/src/components/room/RoomChatPanel.tsx b/src/components/room/RoomChatPanel.tsx
index 961cc84..09094a5 100644
--- a/src/components/room/RoomChatPanel.tsx
+++ b/src/components/room/RoomChatPanel.tsx
@@ -1,5 +1,6 @@
-import type { RoomResponse, RoomMemberResponse, RoomMessageResponse, RoomThreadResponse } from '@/client';
+import type { ProjectRepositoryItem, RoomResponse, RoomMemberResponse, RoomMessageResponse, RoomThreadResponse } from '@/client';
import { useRoom, type MessageWithMeta } from '@/contexts';
+import { type RoomAiConfig } from '@/contexts/room-context';
import { useRoomDraft } from '@/hooks/useRoomDraft';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@@ -30,7 +31,7 @@ const MENTION_PATTERN = /@([^:@\s]*)(:([^\s]*))?$/;
const MENTION_POPOVER_KEYS = ['Enter', 'Tab', 'ArrowUp', 'ArrowDown'];
export interface ChatInputAreaHandle {
- insertMention: (name: string, type: 'user' | 'ai') => void;
+ insertMention: (id: string, label: string, type: 'user' | 'ai') => void;
}
interface ChatInputAreaProps {
@@ -38,6 +39,8 @@ interface ChatInputAreaProps {
onSend: (content: string) => void;
isSending: boolean;
members: RoomMemberResponse[];
+ repos?: ProjectRepositoryItem[];
+ aiConfigs?: RoomAiConfig[];
replyingTo?: { id: string; display_name?: string; content: string } | null;
onCancelReply?: () => void;
draft: string;
@@ -51,6 +54,8 @@ const ChatInputArea = memo(function ChatInputArea({
onSend,
isSending,
members,
+ repos,
+ aiConfigs,
replyingTo,
onCancelReply,
draft,
@@ -75,11 +80,14 @@ const ChatInputArea = memo(function ChatInputArea({
}, [onDraftChange]);
useImperativeHandle(ref, () => ({
- insertMention: (name: string) => {
+ insertMention: (id: string, label: string) => {
if (!textareaRef.current) return;
const value = textareaRef.current.value;
const cursorPos = textareaRef.current.selectionStart;
- const mentionText = `${name} `;
+ // Build new HTML mention: label
+ const escapedLabel = label.replace(//g, '>');
+ const escapedId = id.replace(/"/g, '"');
+ const mentionText = `${escapedLabel} `;
const before = value.substring(0, cursorPos);
const after = value.substring(cursorPos);
const newValue = before + mentionText + after;
@@ -169,6 +177,8 @@ const ChatInputArea = memo(function ChatInputArea({
{showMentionPopover && (
(null);
@@ -322,8 +334,8 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
);
// Stable: chatInputRef is stable, no deps that change on message updates
- const handleMention = useCallback((name: string, type: 'user' | 'ai') => {
- chatInputRef.current?.insertMention(name, type);
+ const handleMention = useCallback((id: string, label: string) => {
+ chatInputRef.current?.insertMention(id, label, 'user');
}, []);
const handleSelectSearchResult = useCallback((message: RoomMessageResponse) => {
@@ -530,6 +542,8 @@ export function RoomChatPanel({ room, isAdmin, onClose, onDelete }: RoomChatPane
onSend={handleSend}
isSending={false}
members={members}
+ repos={projectRepos}
+ aiConfigs={roomAiConfigs}
replyingTo={replyingTo ? { id: replyingTo.id, display_name: replyingTo.display_name ?? undefined, content: replyingTo.content } : null}
onCancelReply={() => setReplyingTo(null)}
draft={draft}
diff --git a/src/components/room/RoomParticipantsPanel.tsx b/src/components/room/RoomParticipantsPanel.tsx
index 2893419..5a675f7 100644
--- a/src/components/room/RoomParticipantsPanel.tsx
+++ b/src/components/room/RoomParticipantsPanel.tsx
@@ -9,7 +9,7 @@ import type { ReactNode } from 'react';
interface RoomParticipantsPanelProps {
members: RoomMemberResponse[];
membersLoading: boolean;
- onMention?: (name: string, type: 'user' | 'ai') => void;
+ onMention?: (id: string, label: string, type: 'user' | 'ai') => void;
}
export const RoomParticipantsPanel = memo(function RoomParticipantsPanel({
@@ -97,7 +97,7 @@ function ParticipantRow({
onMention,
}: {
member: RoomMemberResponse;
- onMention?: (name: string, type: 'user' | 'ai') => void;
+ onMention?: (id: string, label: string, type: 'user' | 'ai') => void;
}) {
const username = member.user_info?.username ?? member.user;
const avatarUrl = member.user_info?.avatar_url;
@@ -110,7 +110,7 @@ function ParticipantRow({
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted/60',
)}
- onClick={() => onMention?.(username, 'user')}
+ onClick={() => onMention?.(member.user, username, 'user')}
disabled={!onMention}
>
diff --git a/src/components/room/RoomSettingsPanel.tsx b/src/components/room/RoomSettingsPanel.tsx
index 9008cb6..947edf9 100644
--- a/src/components/room/RoomSettingsPanel.tsx
+++ b/src/components/room/RoomSettingsPanel.tsx
@@ -1,5 +1,5 @@
import { memo, useState, useEffect, useCallback } from 'react';
-import type { ModelResponse, RoomResponse } from '@/client';
+import type { ModelResponse, RoomResponse, RoomAiResponse, RoomAiUpsertRequest } from '@/client';
import { aiList, aiUpsert, aiDelete, modelList } from '@/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -15,7 +15,6 @@ import {
} from '@/components/ui/dialog';
import { Loader2, Plus, Trash2, Bot, ChevronDown, ChevronRight } from 'lucide-react';
import { toast } from 'sonner';
-import type { ModelResponse, RoomAiResponse, RoomAiUpsertRequest } from '@/client';
interface RoomSettingsPanelProps {
room: RoomResponse;
diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx
index ccf63a3..9412b84 100644
--- a/src/contexts/room-context.tsx
+++ b/src/contexts/room-context.tsx
@@ -11,6 +11,7 @@ import {
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import {
+ type ProjectRepositoryItem,
type RoomCategoryResponse,
type RoomMemberResponse,
type RoomMessageResponse,
@@ -39,6 +40,11 @@ import {
export type { RoomWsStatus, RoomWsClient } from '@/lib/room-ws-client';
+export interface RoomAiConfig {
+ model: string;
+ modelName?: string;
+}
+
export interface ReactionGroup {
emoji: string;
count: number;
@@ -146,6 +152,11 @@ interface RoomContextValue {
updateRoom: (roomId: string, name?: string, isPublic?: boolean, categoryId?: string) => Promise;
deleteRoom: (roomId: string) => Promise;
streamingMessages: Map;
+
+ /** Project repositories for @repository: mention suggestions */
+ projectRepos: ProjectRepositoryItem[];
+ /** Room AI configs for @ai: mention suggestions */
+ roomAiConfigs: RoomAiConfig[];
}
const RoomContext = createContext(null);
@@ -415,6 +426,13 @@ export function RoomProvider({
const [streamingContent, setStreamingContent] = useState