mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(native-chat): keep definition-shaped user lines out of Markdown's mouth
A preserved literal marker can still vanish one layer later: CommonMark reads `[Image #1]: /tmp/a.png` as a link reference definition, so a user turn that is only that line renders as an empty bubble. Escape the line-leading bracket on user turns before they reach the Markdown renderer. Assistant prose is untouched, and the escape is output-neutral everywhere else, including inside fenced code.
This commit is contained in:
@@ -17,6 +17,7 @@ import { stripNoiseMessages } from './native-chat-noise'
|
||||
import { foldToolMessages, splitNativeChatBlocks } from './native-chat-tool-fold'
|
||||
import { isNearBottom, shouldShowJumpToLatest, type ScrollGeometry } from './native-chat-autoscroll'
|
||||
import { isNativeChatPastedImagePath } from './native-chat-image-paste'
|
||||
import { escapeNativeChatUserMarkdown } from './native-chat-user-markdown'
|
||||
import { NativeChatToolRun } from './NativeChatToolRun'
|
||||
import { NativeChatCopyButton } from './NativeChatCopyButton'
|
||||
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat-streaming'
|
||||
@@ -174,7 +175,7 @@ function MessageRow({
|
||||
<>
|
||||
<ImageAttachmentRefs blocks={prose} />
|
||||
<CommentMarkdown
|
||||
content={markdown}
|
||||
content={escapeNativeChatUserMarkdown(markdown)}
|
||||
variant="document"
|
||||
className="text-sm"
|
||||
onLinkClick={onLinkClick}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import CommentMarkdown from '@/components/sidebar/CommentMarkdown'
|
||||
import { escapeNativeChatUserMarkdown } from './native-chat-user-markdown'
|
||||
|
||||
function renderedText(content: string): string {
|
||||
return renderToStaticMarkup(<CommentMarkdown variant="document" content={content} />)
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
describe('escapeNativeChatUserMarkdown', () => {
|
||||
// The swallow this guards against: without the escape the marker line is a
|
||||
// CommonMark link reference definition and the whole turn renders empty.
|
||||
it('keeps a marker line the raw Markdown pipeline drops entirely', () => {
|
||||
const typed = '[Image #1]: /tmp/a.png'
|
||||
|
||||
expect(renderedText(typed)).toBe('')
|
||||
expect(renderedText(escapeNativeChatUserMarkdown(typed))).toBe('[Image #1]: /tmp/a.png')
|
||||
})
|
||||
|
||||
it('keeps a definition-shaped line that precedes other prose', () => {
|
||||
const typed = '[Image #1]: /tmp/a.png\n\nwhat do you make of it?'
|
||||
|
||||
expect(renderedText(typed)).toBe('what do you make of it?')
|
||||
expect(renderedText(escapeNativeChatUserMarkdown(typed))).toBe(
|
||||
'[Image #1]: /tmp/a.png\nwhat do you make of it?'
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'keep [Image #1] literal',
|
||||
'[Image #1] describe this',
|
||||
'here is my note\n[Image #1]: /tmp/a.png\nend',
|
||||
'[Image #1]: /tmp/a.png and more text after',
|
||||
'```\n[Image #1]: /tmp/a.png\n```',
|
||||
'- [Image #1]: /tmp/a.png',
|
||||
'plain prose with a colon: like this'
|
||||
])('renders text the pipeline already showed identically', (typed) => {
|
||||
expect(renderedText(escapeNativeChatUserMarkdown(typed))).toBe(renderedText(typed))
|
||||
})
|
||||
|
||||
it.each(['keep [Image #1] literal', '```\n[Image #1]: /tmp/a.png\n```'])(
|
||||
'leaves lines that cannot open a definition byte-identical',
|
||||
(typed) => {
|
||||
expect(escapeNativeChatUserMarkdown(typed)).toBe(typed)
|
||||
}
|
||||
)
|
||||
|
||||
it('escapes only the line-leading bracket, once per line', () => {
|
||||
expect(escapeNativeChatUserMarkdown('[a]: x\n[b]: y')).toBe('\\[a]: x\n\\[b]: y')
|
||||
expect(escapeNativeChatUserMarkdown(' [a]: x')).toBe(' \\[a]: x')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
const LINK_REFERENCE_DEFINITION_LINE = /^( {0,3})\[(?=[^\]\n]*\]:)/
|
||||
const FENCE_LINE = /^ {0,3}(?:`{3,}|~{3,})/
|
||||
|
||||
/**
|
||||
* A user turn is prose, not authored Markdown. CommonMark reads a line like
|
||||
* `[Image #1]: /tmp/a.png` as a link reference definition and renders nothing at
|
||||
* all, so a marker the transcript rules preserve would still vanish on screen.
|
||||
* Escaping the opening bracket is output-neutral for every other line.
|
||||
*/
|
||||
export function escapeNativeChatUserMarkdown(markdown: string): string {
|
||||
if (!markdown.includes(']:')) {
|
||||
return markdown
|
||||
}
|
||||
let inFence = false
|
||||
return markdown
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (FENCE_LINE.test(line)) {
|
||||
inFence = !inFence
|
||||
return line
|
||||
}
|
||||
return inFence ? line : line.replace(LINK_REFERENCE_DEFINITION_LINE, '$1\\[')
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
Reference in New Issue
Block a user