Add ability to copy table and copy code in messages#791
Add ability to copy table and copy code in messages#791eldong wants to merge 3 commits intoDevelopmentfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds richer rendering and copy affordances for LLM output in the chat UI, primarily by expanding the message preprocessing pipeline (tables + code) and introducing table copy toolbars.
Changes:
- Introduces a unified preprocessing pipeline for chat markdown to normalize multiple table formats and wrap unfenced code blocks.
- Uses the preprocessing pipeline during streaming renders to avoid “raw pipe table” flashes.
- Adds “Copy table” toolbars (TSV copy) and updates chat CSS to support copy-button layout; bumps app version and adds release notes.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/explanation/release_notes.md | Adds v0.239.011 release notes for table rendering + table copy buttons. |
| application/single_app/static/js/chat/chat-streaming.js | Applies preprocessing before marked/DOMPurify rendering during streaming. |
| application/single_app/static/js/chat/chat-messages.js | Adds preprocessing helpers, exports preprocessTableContent, adds table copy buttons, updates render pipeline. |
| application/single_app/static/css/chats.css | Adds table copy toolbar styling and adds top padding to code blocks. |
| application/single_app/config.py | Bumps VERSION to 0.239.011. |
You can also share your feedback on Copilot code review. Take the survey.
| const hasTablePipes = /\|.+\|/.test(withTableSeparators); | ||
| const hasHtmlTable = sanitizedHtml.includes('<table'); | ||
| if (hasTablePipes) { | ||
| console.log(`📊 Table diagnostic (msg ${messageId}): pipes=${hasTablePipes}, htmlTable=${hasHtmlTable}`); | ||
| if (!hasHtmlTable) { | ||
| console.warn('⚠️ Content has pipe chars but no <table> was generated. Content preview:', withTableSeparators.substring(0, 300)); |
There was a problem hiding this comment.
The new table diagnostics can log potentially sensitive message content (first 300 chars) into the browser console and will be noisy for any message containing pipe characters. Please gate this behind an explicit debug flag (e.g., app setting / query param) or remove the content preview logging for production.
| const hasTablePipes = /\|.+\|/.test(withTableSeparators); | |
| const hasHtmlTable = sanitizedHtml.includes('<table'); | |
| if (hasTablePipes) { | |
| console.log(`📊 Table diagnostic (msg ${messageId}): pipes=${hasTablePipes}, htmlTable=${hasHtmlTable}`); | |
| if (!hasHtmlTable) { | |
| console.warn('⚠️ Content has pipe chars but no <table> was generated. Content preview:', withTableSeparators.substring(0, 300)); | |
| const enableTableDiagnostics = | |
| typeof window !== "undefined" && | |
| window.appSettings && | |
| window.appSettings.enable_table_diagnostics === true; | |
| const hasTablePipes = /\|.+\|/.test(withTableSeparators); | |
| const hasHtmlTable = sanitizedHtml.includes("<table"); | |
| if (enableTableDiagnostics && hasTablePipes) { | |
| console.log( | |
| `📊 Table diagnostic (msg ${messageId}): pipes=${hasTablePipes}, htmlTable=${hasHtmlTable}` | |
| ); | |
| if (!hasHtmlTable) { | |
| console.warn( | |
| "⚠️ Content has pipe chars but no <table> was generated.", | |
| { messageId, hasTablePipes, hasHtmlTable } | |
| ); |
There was a problem hiding this comment.
please update based on suggestion or think through another way to limit exposure
| const sep = '|' + cells.map(() => ' --- ').join('|') + '|'; | ||
| result.push(sep); | ||
| insideTable = true; | ||
| console.log('🔧 Inserted missing table separator after:', trimmed.substring(0, 80)); |
There was a problem hiding this comment.
Table preprocessing currently logs to console on normal user flows (Inserted missing table separator…). This will spam DevTools during streaming/non-streaming rendering. Consider removing these logs or guarding them behind a debug flag.
| console.log('🔧 Inserted missing table separator after:', trimmed.substring(0, 80)); |
There was a problem hiding this comment.
please update based on suggestion or think through another way to limit exposure
| navigator.clipboard.writeText(tsv.trim()).then(() => { | ||
| // Update both buttons | ||
| topBtn.innerHTML = '<i class="bi bi-check-lg text-success"></i> Copied!'; | ||
| bottomBtn.innerHTML = '<i class="bi bi-check-lg text-success"></i> Copied!'; | ||
| setTimeout(() => { | ||
| topBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy table'; | ||
| bottomBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy table'; | ||
| }, 2000); | ||
| }).catch(err => { | ||
| console.error('Failed to copy table:', err); | ||
| }); |
There was a problem hiding this comment.
attachTableCopyButtons swallows clipboard failures (console error only). For consistency with attachCodeBlockCopyButtons, please show a user-visible toast on failure and consider a fallback path when navigator.clipboard is unavailable (non-secure contexts / permissions).
There was a problem hiding this comment.
please update based on suggestion
| display: block; | ||
| /* Optional: prevent code from wrapping */ | ||
| white-space: pre; | ||
| position: relative; |
There was a problem hiding this comment.
pre, pre[class*="language-"] now gets padding-top: 2.2rem globally to make room for a copy button. This will add extra whitespace to code blocks anywhere in the app, even when no copy button is injected. Consider scoping this to chat messages (e.g., .message-text pre...) or applying padding only when .copy-code-btn is present.
| position: relative; | |
| position: relative; | |
| } | |
| /* Apply extra top padding only to code blocks inside chat messages | |
| to make room for the copy button */ | |
| .message-text pre, | |
| .message-text pre[class*="language-"] { |
There was a problem hiding this comment.
please update based on suggestion
| if (contentElement) { | ||
| // Render markdown during streaming for proper formatting | ||
| if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { | ||
| const renderedContent = DOMPurify.sanitize(marked.parse(content)); | ||
| const preprocessed = preprocessTableContent(content); | ||
| const renderedContent = DOMPurify.sanitize(marked.parse(preprocessed)); | ||
| contentElement.innerHTML = renderedContent; |
There was a problem hiding this comment.
updateStreamingMessage() now runs the full preprocessing pipeline on every streaming chunk. That pipeline includes several full-string passes (multiple regex conversions + wrapUnfencedCodeBlocks), which can get expensive and cause UI jank on long responses. Consider using a lighter streaming-only preprocessing (tables only), throttling updates, or deferring heavy steps until finalizeStreamingMessage().
| // Parse content with comprehensive table processing | ||
| let cleaned = messageContent.trim().replace(/\n{3,}/g, "\n\n"); | ||
| cleaned = cleaned.replace(/(\bhttps?:\/\/\S+)(%5D|\])+/gi, (_, url) => url); | ||
| const withInlineCitations = parseCitations(cleaned); | ||
| const withUnwrappedTables = unwrapTablesFromCodeBlocks(withInlineCitations); | ||
| const withMarkdownTables = convertUnicodeTableToMarkdown(withUnwrappedTables); | ||
| const withPSVTables = convertPSVCodeBlockToMarkdown(withMarkdownTables); | ||
| const withASCIITables = convertASCIIDashTableToMarkdown(withPSVTables); | ||
| const sanitizedHtml = DOMPurify.sanitize(marked.parse(withASCIITables)); | ||
| const withTableSeparators = preprocessTableContent(withInlineCitations); | ||
| const sanitizedHtml = DOMPurify.sanitize(marked.parse(withTableSeparators)); | ||
| const htmlContent = addTargetBlankToExternalLinks(sanitizedHtml); |
There was a problem hiding this comment.
Existing functional tests hard-code the old table-processing pipeline strings (e.g., unwrapTablesFromCodeBlocks(withInlineCitations)); with the new preprocessTableContent() call, those tests will fail if they’re part of the validation suite (see functional_tests/test_comprehensive_table_support.py and functional_tests/test_final_table_validation.py). Please update the tests/validators to assert the new pipeline entry point instead.
No description provided.