From c1d167bd647297a5995e627a7dac56c6cfce4e16 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 May 2026 13:00:27 +0530 Subject: [PATCH] fix: prevent `--` signature delimiter rendering as `\` in bubble (#14134) # Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-h2-when-using-enter-line-before **1**. Fixes an issue where the signature delimiter `--` gets parsed as an H2 when using **Enter** (new paragraph) before or after it, causing it to render as a bold `\` in the message bubble. * Ensures `--` renders as plain text * Aligns renderer with parser behavior (both disable `lheading`) * Prevents stray `\` from appearing as heading text **2**. Also fixes a related editor issue where toggling signature **off** leaves behind a stray `\` or `-- \`. * Strips blank paragraph markers (`\`) and dangling hard breaks (`\`) from ProseMirror serializer * Applied in both `appendSignature` and `removeSignature` * Replaces `trimEnd()` with shared helpers (`trimTrailingBlanks` / `stripTrailingBlankMarkers`) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? #### Screenshots **Before** image **After** image **Editor** https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-in-h2-when-using-enter-line#comment-5814b882 ### Steps #### Editor 1. Enable agent signature 2. Add and remove new lines around the signature using Enter/shift enter 3. Toggle signature off 4. Notice stray `\` or `-- \` remains #### Bubble 1. Enable agent signature 2. Send a message using Enter between lines 3. Verify `--` renders correctly (no H2, no bold `\`) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth --- .../dashboard/helper/editorHelper.js | 15 ++++++++- .../helper/specs/editorHelper.spec.js | 32 +++++++++++++++++++ .../shared/helpers/MessageFormatter.js | 1 + .../helpers/specs/MessageFormatter.spec.js | 7 ++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/helper/editorHelper.js b/app/javascript/dashboard/helper/editorHelper.js index 9839a04e0..491dd9d44 100644 --- a/app/javascript/dashboard/helper/editorHelper.js +++ b/app/javascript/dashboard/helper/editorHelper.js @@ -123,6 +123,13 @@ export function cleanSignature(signature) { } } +// Strip `\` hardbreak markers trailing `--` after a signature slice +const stripDelimiterHardbreaks = body => + body.replace(/(--)\s*(?:\\\s*)+$/, '$1'); + +// Strip standalone blank-paragraph markers (`\` on their own lines). +const stripTrailingBlankLine = body => body.replace(/\n(?:\s*\\\n)+$/, ''); + /** * Adds the signature delimiter to the beginning of the signature. * @@ -227,13 +234,19 @@ export function removeSignature(body, signature, channelType) { // trimming will ensure any spaces or new lines before the signature are removed // This means we will have the delimiter at the end if (signatureIndex > -1) { - newBody = newBody.substring(0, signatureIndex).trimEnd(); + newBody = stripDelimiterHardbreaks( + newBody.substring(0, signatureIndex) + ).trimEnd(); } // Remove delimiter if it's at the end if (newBody.endsWith(SIGNATURE_DELIMITER)) { // if the delimiter is at the end, remove it newBody = newBody.slice(0, -SIGNATURE_DELIMITER.length); + // strip any trailing blank-line markers + if (signatureIndex > -1) { + newBody = stripTrailingBlankLine(newBody); + } } return newBody; diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js index b06c36d42..c7822bde7 100644 --- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js @@ -336,6 +336,38 @@ describe('removeSignature', () => { 'This is a test\n\n' ); }); + it('strips blank-paragraph marker before the delimiter', () => { + expect(removeSignature('hey\n\n\\\n--\n\nHello there', 'Hello there')).toBe( + 'hey' + ); + }); + it('strips multiple consecutive blank-paragraph markers before the delimiter', () => { + expect( + removeSignature('wewe\n\n\\\n\\\n\\\n--\n\nHello there', 'Hello there') + ).toBe('wewe'); + }); + it('strips dangling hardbreak when signature shared a paragraph with "--"', () => { + expect(removeSignature('hey\n\n--\\\nHello there', 'Hello there')).toBe( + 'hey\n\n' + ); + }); + it('preserves trailing backslash in user text when appending', () => { + expect(appendSignature('The path is C:\\', 'Best\nAgent')).toContain( + 'C:\\' + ); + expect(appendSignature('C:\\\n', 'Best\nAgent')).toContain('C:\\'); + expect(appendSignature('C:\\\n\n', 'Best\nAgent')).toContain('C:\\'); + }); + it('preserves trailing backslash in user text when removing', () => { + expect(removeSignature('C:\\\n--\n\nBest\nAgent', 'Best\nAgent')).toContain( + 'C:\\' + ); + expect(removeSignature('C:\\\n--', 'no matching sig')).toContain('C:\\'); + expect(removeSignature('C:\\\nBest\\\nAgent', 'Best\nAgent')).toContain( + 'C:\\' + ); + expect(removeSignature('notes\n\\\n--', 'no matching sig')).toContain('\\'); + }); }); describe('removeSignature with stripped signature', () => { diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js index c87209fd4..825132e13 100644 --- a/app/javascript/shared/helpers/MessageFormatter.js +++ b/app/javascript/shared/helpers/MessageFormatter.js @@ -42,6 +42,7 @@ const createMarkdownInstance = (linkify = true) => { quotes: '\u201c\u201d\u2018\u2019', maxNesting: 20, }) + .disable(['lheading']) .use(mentionPlugin) .use(imgResizeManager) .use(mila, { diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index a685cb0da..12b84085c 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -33,6 +33,13 @@ describe('#MessageFormatter', () => {

tool

` ); }); + + it('should not render a setext heading when text is followed by "--"', () => { + const message = 'hy\n\n\\\n\\-\\-\n\nHello there'; + const result = new MessageFormatter(message).formattedMessage; + expect(result).not.toMatch('

'); + expect(result).not.toMatch('

'); + }); }); describe('content with image and has "cw_image_height" query at the end of URL', () => {