From 98457500a66f3d0a9bdc12bb3876d8aa5e7e6155 Mon Sep 17 00:00:00 2001 From: gfpyc Date: Fri, 14 Aug 2026 20:24:37 +0800 Subject: [PATCH 1/2] fix(desktop): respect explicit language tags in code-block prose detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XpycT's repro matrix (text/markdown/gdscript/yaml fences whose content looks prose-like, >=3 lines with zero code signals) is still broken on main: the prose heuristics strip the fence and collapse the block into a flat paragraph. The recent isLikelyStructuredText guard only protects config/structured listings, not explicit-tag fences with prose-looking content. Respect any explicit, valid language tag in both isLikelyProseFence and isLikelyProseCodeBlock: a fenced block with a real tag (text, plain, plaintext, markdown, md, gdscript, yaml, ...) renders as code regardless of how prose-like the body looks. Bare fences ('') keep the existing behavior — untagged paragraph fences still unwrap, and config/key-value listings stay fenced via isLikelyStructuredText. Also exempt COMMON code languages from the bullet-prose heuristic in isLikelyProseCodeBlock so yaml/markdown lists render as code, while unknown tags (e.g. Streamdown's 'heads') still classify as prose. --- apps/desktop/src/lib/markdown-code.test.ts | 59 ++++++++++++++++++++++ apps/desktop/src/lib/markdown-code.ts | 28 +++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/lib/markdown-code.test.ts b/apps/desktop/src/lib/markdown-code.test.ts index ec12187f2c31..915de28f1b3a 100644 --- a/apps/desktop/src/lib/markdown-code.test.ts +++ b/apps/desktop/src/lib/markdown-code.test.ts @@ -2,6 +2,19 @@ import { describe, expect, it } from 'vitest' import { isLikelyProseCodeBlock, isLikelyProseFence, isLikelyStructuredText } from './markdown-code' +// Explicit-tag regression matrix (XpycT's four-block repro on Windows 11, +// Hermes v0.19.1): text/markdown/gdscript/yaml fences whose content looks +// prose-like (>=3 lines, no code signals) must stay code blocks. Only bare +// fences ('') still fall through to the prose heuristic. +const LOREM_3 = ['lorem ipsum dolor sit amet', 'consectetur adipiscing elit', 'sed do eiusmod tempor'].join('\n') +const SENTENCE_3 = [ + 'The quarterly report shows steady growth across all sectors.', + 'Regional performance exceeded expectations by twelve percent.', + 'Customer retention rates improved for the third consecutive month.' +].join('\n') +const MARKDOWN_BLOCK = ['# Deployment notes', '', 'This block should stay fenced'].join('\n') +const YAML_LIST = ['providers:', ' - name: deepseek', ' - name: openai'].join('\n') + describe('isLikelyProseCodeBlock', () => { it('detects prose that Streamdown mislabels as an unknown language', () => { expect( @@ -35,6 +48,29 @@ describe('isLikelyProseCodeBlock', () => { it('keeps an .env-style dump fenced', () => { expect(isLikelyProseCodeBlock('', ['API_KEY=abc123', 'PORT=8080', 'DEBUG=true'].join('\n'))).toBe(false) }) + + it('keeps text fences as code even with zero code signals', () => { + expect(isLikelyProseCodeBlock('text', LOREM_3)).toBe(false) + expect(isLikelyProseCodeBlock('text', SENTENCE_3)).toBe(false) + }) + + it('keeps plain/plaintext fences as code', () => { + expect(isLikelyProseCodeBlock('plain', LOREM_3)).toBe(false) + expect(isLikelyProseCodeBlock('plaintext', SENTENCE_3)).toBe(false) + }) + + it('keeps markdown/md fences as code instead of rich-rendering them', () => { + expect(isLikelyProseCodeBlock('markdown', MARKDOWN_BLOCK)).toBe(false) + expect(isLikelyProseCodeBlock('md', SENTENCE_3)).toBe(false) + }) + + it('keeps gdscript as code (non-COMMON explicit tag)', () => { + expect(isLikelyProseCodeBlock('gdscript', SENTENCE_3)).toBe(false) + }) + + it('keeps yaml bullet lists as code', () => { + expect(isLikelyProseCodeBlock('yaml', YAML_LIST)).toBe(false) + }) }) describe('isLikelyStructuredText', () => { @@ -92,4 +128,27 @@ describe('isLikelyProseFence', () => { ) ).toBe(true) }) + + it('keeps text fences as code', () => { + expect(isLikelyProseFence('text', LOREM_3)).toBe(false) + expect(isLikelyProseFence('text', SENTENCE_3)).toBe(false) + }) + + it('keeps plain/plaintext fences as code', () => { + expect(isLikelyProseFence('plain', LOREM_3)).toBe(false) + expect(isLikelyProseFence('plaintext', SENTENCE_3)).toBe(false) + }) + + it('keeps markdown/md fences as code instead of rich-rendering them', () => { + expect(isLikelyProseFence('markdown', MARKDOWN_BLOCK)).toBe(false) + expect(isLikelyProseFence('md', SENTENCE_3)).toBe(false) + }) + + it('keeps gdscript as code (non-COMMON explicit tag)', () => { + expect(isLikelyProseFence('gdscript', SENTENCE_3)).toBe(false) + }) + + it('keeps yaml bullet lists as code', () => { + expect(isLikelyProseFence('yaml', YAML_LIST)).toBe(false) + }) }) diff --git a/apps/desktop/src/lib/markdown-code.ts b/apps/desktop/src/lib/markdown-code.ts index ccad28422873..565456e5f5e5 100644 --- a/apps/desktop/src/lib/markdown-code.ts +++ b/apps/desktop/src/lib/markdown-code.ts @@ -361,6 +361,16 @@ export function isLikelyProseFence(info: string, body: string): boolean { return true } + // Any explicit, valid language tag is respected as code. sanitizeLanguageTag + // only returns non-empty for tags matching VALID_LANGUAGE_RE, so this covers + // text/plain/plaintext/md/markdown (in NON_CODE_FENCE_LANGUAGES) as well as + // gdscript/zsh and other non-COMMON tags the fallback below would otherwise + // prose-classify. Bare fences ('') skip this guard and fall through to the + // structured/prose heuristics so untagged paragraph fences still unwrap. + if (language !== '' && VALID_LANGUAGE_RE.test(language)) { + return false + } + if (!NON_CODE_FENCE_LANGUAGES.has(language)) { return false } @@ -388,8 +398,13 @@ export function isLikelyProseCodeBlock(language: string | undefined, code: strin // A bullet list with markdown emphasis is prose even when it happens to be // structured; the config veto below is only meant to protect config/kv - // listings, so let the bullet-prose case win first. - if (signals.bulletLines >= 1 && (signals.hasMarkdown || signals.proseLines >= 2)) { + // listings, so let the bullet-prose case win first. Known code languages + // (yaml/markdown) are exempt: their lists are data, not prose. + if ( + signals.bulletLines >= 1 && + (signals.hasMarkdown || signals.proseLines >= 2) && + !COMMON_CODE_LANGUAGES.has(cleanLanguage) + ) { return true } @@ -399,6 +414,15 @@ export function isLikelyProseCodeBlock(language: string | undefined, code: strin return false } + // Any explicit, valid language tag is respected and rendered as code + // (covers gdscript/zsh and other non-COMMON languages, plus text/plain/ + // plaintext/md/markdown). sanitizeLanguageTag returning non-empty means + // VALID_LANGUAGE_RE matched; only bare fences ('') and unknown tags fall + // through to the prose heuristic below. + if (cleanLanguage !== '' && VALID_LANGUAGE_RE.test(cleanLanguage)) { + return false + } + if (NON_CODE_FENCE_LANGUAGES.has(cleanLanguage)) { return signals.proseLines >= 3 && signals.codeSignals === 0 } From 1f51c27bb6f917d8b95ae6a05bcfef48d2a35934 Mon Sep 17 00:00:00 2001 From: gfpyc Date: Sat, 15 Aug 2026 01:26:54 +0800 Subject: [PATCH 2/2] fix(desktop): respect explicit tags for bullet-list code blocks The explicit-tag guard ran after the bullet-prose heuristic, so non-COMMON languages (gdscript/zsh) and NON_CODE tags (text/plain/plaintext) with bullet-list bodies were still prose-classified before the guard was reached. Move the explicit-tag guard ahead of the bullet heuristic, keeping the Streamdown 'heads' case (bullet + inline markdown emphasis) as prose per the upstream test. Adds whole-class bullet-list coverage plus the numbered Chinese prose repro reported on macOS (text/prompt tags). --- apps/desktop/src/lib/markdown-code.test.ts | 58 ++++++++++++++++++++++ apps/desktop/src/lib/markdown-code.ts | 13 +++++ 2 files changed, 71 insertions(+) diff --git a/apps/desktop/src/lib/markdown-code.test.ts b/apps/desktop/src/lib/markdown-code.test.ts index 915de28f1b3a..339c96e3b9dc 100644 --- a/apps/desktop/src/lib/markdown-code.test.ts +++ b/apps/desktop/src/lib/markdown-code.test.ts @@ -14,6 +14,17 @@ const SENTENCE_3 = [ ].join('\n') const MARKDOWN_BLOCK = ['# Deployment notes', '', 'This block should stay fenced'].join('\n') const YAML_LIST = ['providers:', ' - name: deepseek', ' - name: openai'].join('\n') +const BULLET_2 = ['- item one', '- item two'].join('\n') + +// Stone441 macOS repro (2026-08-14): long `text`/`prompt` fences with +// numbered Chinese prose were downgraded to rendered prose. Numbered lines +// ("1. 中文…") match proseLineCount's ASCII-leading regex, so they trip the +// prose heuristic; the explicit-tag guard must keep them fenced. +const CHINESE_NUMBERED = [ + '1. 这是一个较长的中文提示词,包含编号和具体步骤说明。', + '2. 第二行继续描述用户希望保留的完整提示词内容。', + '3. 第三行用于验证换行、编号和空格在代码块内保持原样。' +].join('\n') describe('isLikelyProseCodeBlock', () => { it('detects prose that Streamdown mislabels as an unknown language', () => { @@ -71,6 +82,34 @@ describe('isLikelyProseCodeBlock', () => { it('keeps yaml bullet lists as code', () => { expect(isLikelyProseCodeBlock('yaml', YAML_LIST)).toBe(false) }) + + // Whole-class regression (spfcraze triage, 2026-08-14): non-COMMON explicit + // tags with bullet-list bodies were prose-classified by the bullet heuristic + // before the explicit-tag guard was reached. gdscript/zsh are non-COMMON, + // text/plain/plaintext are NON_CODE (not COMMON) — all must stay code. + it('keeps gdscript bullet lists as code (spfcraze repro)', () => { + expect(isLikelyProseCodeBlock('gdscript', BULLET_2)).toBe(false) + }) + + it('keeps zsh bullet lists as code (spfcraze repro)', () => { + expect(isLikelyProseCodeBlock('zsh', BULLET_2)).toBe(false) + }) + + it('keeps NON_CODE-family bullet lists as code (text/plain/plaintext)', () => { + expect(isLikelyProseCodeBlock('text', BULLET_2)).toBe(false) + expect(isLikelyProseCodeBlock('plain', BULLET_2)).toBe(false) + expect(isLikelyProseCodeBlock('plaintext', BULLET_2)).toBe(false) + }) + + // Stone441 macOS repro: numbered Chinese prose inside text/prompt fences + // must stay code blocks (the numbered lines trip the prose heuristic). + it('keeps text fences with numbered Chinese prose as code (Stone441)', () => { + expect(isLikelyProseCodeBlock('text', CHINESE_NUMBERED)).toBe(false) + }) + + it('keeps prompt fences with numbered Chinese prose as code (Stone441)', () => { + expect(isLikelyProseCodeBlock('prompt', CHINESE_NUMBERED)).toBe(false) + }) }) describe('isLikelyStructuredText', () => { @@ -151,4 +190,23 @@ describe('isLikelyProseFence', () => { it('keeps yaml bullet lists as code', () => { expect(isLikelyProseFence('yaml', YAML_LIST)).toBe(false) }) + + // Whole-class regression (spfcraze triage, 2026-08-14): same bullet-list + // coverage on the fence layer — non-COMMON explicit tags must stay code. + it('keeps gdscript/zsh bullet-list fences as code', () => { + expect(isLikelyProseFence('gdscript', BULLET_2)).toBe(false) + expect(isLikelyProseFence('zsh', BULLET_2)).toBe(false) + }) + + it('keeps NON_CODE-family bullet-list fences as code', () => { + expect(isLikelyProseFence('text', BULLET_2)).toBe(false) + expect(isLikelyProseFence('plain', BULLET_2)).toBe(false) + expect(isLikelyProseFence('plaintext', BULLET_2)).toBe(false) + }) + + // Stone441 macOS repro: text/prompt fences with numbered Chinese prose. + it('keeps text/prompt fences with numbered Chinese prose as code (Stone441)', () => { + expect(isLikelyProseFence('text', CHINESE_NUMBERED)).toBe(false) + expect(isLikelyProseFence('prompt', CHINESE_NUMBERED)).toBe(false) + }) }) diff --git a/apps/desktop/src/lib/markdown-code.ts b/apps/desktop/src/lib/markdown-code.ts index 565456e5f5e5..9afe0f388ab4 100644 --- a/apps/desktop/src/lib/markdown-code.ts +++ b/apps/desktop/src/lib/markdown-code.ts @@ -396,6 +396,19 @@ export function isLikelyProseCodeBlock(language: string | undefined, code: strin return false } + // Any explicit, valid language tag is respected as code — even when the + // body is a bullet list (gdscript/zsh lists were prose-classified by the + // bullet heuristic below, which only exempts COMMON languages). Exception: + // Streamdown-mislabeled unknown tags like "heads" (bullet + inline + // markdown emphasis) stay prose per the upstream test locked in below. + if ( + cleanLanguage !== '' && + VALID_LANGUAGE_RE.test(cleanLanguage) && + !(signals.bulletLines >= 1 && signals.hasMarkdown) + ) { + return false + } + // A bullet list with markdown emphasis is prose even when it happens to be // structured; the config veto below is only meant to protect config/kv // listings, so let the bullet-prose case win first. Known code languages