From 52784073c2ca25295697967813e0d0f0766a80c4 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 13 Dec 2025 17:38:08 +0200 Subject: [PATCH] refactor: simplify mention handling and remove unused code --- CHANGELOG.md | 5 + .../ui/src/components/chat/ChatMessage.tsx | 2 +- .../chat/message/parts/UserTextPart.tsx | 331 ++---------------- 3 files changed, 37 insertions(+), 301 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e667c9d3..639efe4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] + +## [1.1.1] - 2025-12-13 + - Adjusted model/agent selection alignment +- Fixed user message rendering issues + ## [1.1.0] - 2025-12-13 diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index b1c1b9ac..fe173889 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -652,7 +652,7 @@ const ChatMessage: React.FC = ({
= ({ children, className }) => { - const [copied, setCopied] = React.useState(false); - const codeRef = React.useRef(null); - - const handleCopy = async () => { - if (!codeRef.current) return; - const codeEl = codeRef.current.querySelector('code'); - const code = codeEl?.innerText || ''; - if (!code) return; - try { - await navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - return ( -
- {children} -
- -
-
- ); -}; - -// Table utility functions -const extractTableData = (tableEl: HTMLTableElement): { headers: string[]; rows: string[][] } => { - const headers: string[] = []; - const rows: string[][] = []; - - const thead = tableEl.querySelector('thead'); - if (thead) { - const headerCells = thead.querySelectorAll('th'); - headerCells.forEach(cell => headers.push(cell.innerText.trim())); - } - - const tbody = tableEl.querySelector('tbody'); - if (tbody) { - const rowEls = tbody.querySelectorAll('tr'); - rowEls.forEach(row => { - const cells = row.querySelectorAll('td'); - const rowData: string[] = []; - cells.forEach(cell => rowData.push(cell.innerText.trim())); - rows.push(rowData); - }); - } - - return { headers, rows }; -}; - -const tableToCSV = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => { - const escapeCell = (cell: string): string => { - if (cell.includes(',') || cell.includes('"') || cell.includes('\n')) { - return `"${cell.replace(/"/g, '""')}"`; - } - return cell; - }; - - const lines: string[] = []; - if (headers.length > 0) { - lines.push(headers.map(escapeCell).join(',')); - } - rows.forEach(row => lines.push(row.map(escapeCell).join(','))); - return lines.join('\n'); -}; - -const tableToTSV = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => { - const escapeCell = (cell: string): string => { - return cell.replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/\r/g, '\\r'); - }; - - const lines: string[] = []; - if (headers.length > 0) { - lines.push(headers.map(escapeCell).join('\t')); - } - rows.forEach(row => lines.push(row.map(escapeCell).join('\t'))); - return lines.join('\n'); -}; - -const tableToMarkdown = ({ headers, rows }: { headers: string[]; rows: string[][] }): string => { - if (headers.length === 0) return ''; - - const escapeCell = (cell: string): string => { - return cell.replace(/\\/g, '\\\\').replace(/\|/g, '\\|'); - }; - - const lines: string[] = []; - lines.push(`| ${headers.map(escapeCell).join(' | ')} |`); - lines.push(`| ${headers.map(() => '---').join(' | ')} |`); - rows.forEach(row => { - const paddedRow = headers.map((_, i) => escapeCell(row[i] || '')); - lines.push(`| ${paddedRow.join(' | ')} |`); - }); - return lines.join('\n'); -}; - -const downloadFile = (filename: string, content: string, mimeType: string) => { - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -}; - -// Table copy button with dropdown -const TableCopyButton: React.FC<{ tableRef: React.RefObject }> = ({ tableRef }) => { - const [copied, setCopied] = React.useState(false); - const [showMenu, setShowMenu] = React.useState(false); - const menuRef = React.useRef(null); - - React.useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setShowMenu(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const handleCopy = async (format: 'csv' | 'tsv') => { - const tableEl = tableRef.current?.querySelector('table'); - if (!tableEl) return; - - try { - const data = extractTableData(tableEl); - const content = format === 'csv' ? tableToCSV(data) : tableToTSV(data); - await navigator.clipboard.write([ - new ClipboardItem({ - 'text/plain': new Blob([content], { type: 'text/plain' }), - 'text/html': new Blob([tableEl.outerHTML], { type: 'text/html' }), - }), - ]); - setCopied(true); - setShowMenu(false); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error('Failed to copy table:', err); - } - }; - - return ( -
- - {showMenu && ( -
- - -
- )} -
- ); -}; - -// Table download button with dropdown -const TableDownloadButton: React.FC<{ tableRef: React.RefObject }> = ({ tableRef }) => { - const [showMenu, setShowMenu] = React.useState(false); - const menuRef = React.useRef(null); - - React.useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setShowMenu(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const handleDownload = (format: 'csv' | 'markdown') => { - const tableEl = tableRef.current?.querySelector('table'); - if (!tableEl) return; - - try { - const data = extractTableData(tableEl); - const content = format === 'csv' ? tableToCSV(data) : tableToMarkdown(data); - const filename = format === 'csv' ? 'table.csv' : 'table.md'; - const mimeType = format === 'csv' ? 'text/csv' : 'text/markdown'; - downloadFile(filename, content, mimeType); - setShowMenu(false); - } catch (err) { - console.error('Failed to download table:', err); - } - }; - - return ( -
- - {showMenu && ( -
- - -
- )} -
- ); -}; - -// Table wrapper with custom controls -const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> = ({ children, className }) => { - const tableRef = React.useRef(null); - - return ( -
-
- - -
-
- - {children} -
-
-
- ); -}; - -const streamdownComponents = { - pre: CodeBlockWrapper, - table: TableWrapper, -}; type PartWithText = Part & { text?: string; content?: string; value?: string }; @@ -284,9 +13,9 @@ type UserTextPartProps = { agentMention?: AgentMentionInfo; }; -const buildMentionLink = (token: string, name: string): string => { +const buildMentionUrl = (name: string): string => { const encoded = encodeURIComponent(name); - return `[${token}](https://opencode.ai/docs/agents/#${encoded})`; + return `https://opencode.ai/docs/agents/#${encoded}`; }; const UserTextPart: React.FC = ({ part, messageId, isMobile, agentMention }) => { @@ -298,27 +27,12 @@ const UserTextPart: React.FC = ({ part, messageId, isMobile, const [isTruncated, setIsTruncated] = React.useState(false); const textRef = React.useRef(null); - const processedText = React.useMemo(() => { - if (!agentMention) { - return textContent; - } - const token = agentMention.token; - if (!token || token.length === 0) { - return textContent; - } - if (!textContent.includes(token)) { - return textContent; - } - const link = buildMentionLink(token, agentMention.name); - return textContent.replace(token, link); - }, [agentMention, textContent]); - React.useEffect(() => { const el = textRef.current; if (el && !isExpanded) { setIsTruncated(el.scrollHeight > el.clientHeight); } - }, [processedText, isExpanded]); + }, [textContent, isExpanded]); const handleClick = React.useCallback(() => { if (isTruncated || isExpanded) { @@ -326,14 +40,39 @@ const UserTextPart: React.FC = ({ part, messageId, isMobile, } }, [isTruncated, isExpanded]); - if (!processedText || processedText.trim().length === 0) { + if (!textContent || textContent.trim().length === 0) { return null; } + // Render content with optional agent mention link + const renderContent = () => { + if (!agentMention?.token || !textContent.includes(agentMention.token)) { + return textContent; + } + const idx = textContent.indexOf(agentMention.token); + const before = textContent.slice(0, idx); + const after = textContent.slice(idx + agentMention.token.length); + return ( + <> + {before} + e.stopPropagation()} + > + {agentMention.token} + + {after} + + ); + }; + return (
= ({ part, messageId, isMobile, onClick={handleClick} key={part.id || `${messageId}-user-text`} > - - {processedText} - + {renderContent()}
); };