From 0b9849eaef7173d101b88aa9f6ed7522926f8e61 Mon Sep 17 00:00:00 2001 From: LifeJiggy Date: Fri, 29 May 2026 17:32:29 +0100 Subject: [PATCH] fix(ui): improve Markdown text color, code block dimming, and border visibility Apply default text color to plain paragraph content via defaultTextStyle, improve highlightCode fallback to dim unknown-language code blocks, and use textDim for code block borders instead of textMuted. Adds tests for headings, bold, lists to verify no raw markdown syntax leaks into rendered output. Closes #209. --- .changeset/markdown-theme-rendering-fixes.md | 5 +++ .../components/messages/assistant-message.ts | 6 ++- apps/kimi-code/src/tui/theme/pi-tui-theme.ts | 17 ++++---- .../messages/assistant-message.test.ts | 41 ++++++++++++++++++- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 .changeset/markdown-theme-rendering-fixes.md diff --git a/.changeset/markdown-theme-rendering-fixes.md b/.changeset/markdown-theme-rendering-fixes.md new file mode 100644 index 00000000..cd93fb71 --- /dev/null +++ b/.changeset/markdown-theme-rendering-fixes.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Apply text color to plain markdown paragraphs and dim code blocks with unknown fence languages. diff --git a/apps/kimi-code/src/tui/components/messages/assistant-message.ts b/apps/kimi-code/src/tui/components/messages/assistant-message.ts index 1be89b2c..3bca33db 100644 --- a/apps/kimi-code/src/tui/components/messages/assistant-message.ts +++ b/apps/kimi-code/src/tui/components/messages/assistant-message.ts @@ -5,7 +5,7 @@ * to align after the bullet. */ -import type { Component, MarkdownTheme } from '@earendil-works/pi-tui'; +import type { Component, DefaultTextStyle, MarkdownTheme } from '@earendil-works/pi-tui'; import { Container, Markdown, visibleWidth } from '@earendil-works/pi-tui'; import chalk from 'chalk'; @@ -19,12 +19,14 @@ export class AssistantMessageComponent implements Component { private bulletColor: string; private lastText = ''; private showBullet: boolean; + private defaultTextStyle: DefaultTextStyle; constructor(markdownTheme: MarkdownTheme, colors: ColorPalette, showBullet: boolean = true) { this.markdownTheme = markdownTheme; this.bulletColor = colors.roleAssistant; this.showBullet = showBullet; this.contentContainer = new Container(); + this.defaultTextStyle = { color: (text) => chalk.hex(colors.text)(text) }; } setShowBullet(show: boolean): void { @@ -37,7 +39,7 @@ export class AssistantMessageComponent implements Component { this.lastText = displayText; this.contentContainer.clear(); if (displayText.trim().length > 0) { - this.contentContainer.addChild(new Markdown(displayText.trim(), 0, 0, this.markdownTheme)); + this.contentContainer.addChild(new Markdown(displayText.trim(), 0, 0, this.markdownTheme, this.defaultTextStyle)); } } diff --git a/apps/kimi-code/src/tui/theme/pi-tui-theme.ts b/apps/kimi-code/src/tui/theme/pi-tui-theme.ts index dc3b1b9a..8d88eba4 100644 --- a/apps/kimi-code/src/tui/theme/pi-tui-theme.ts +++ b/apps/kimi-code/src/tui/theme/pi-tui-theme.ts @@ -30,7 +30,7 @@ export function createMarkdownTheme(colors: ColorPalette): MarkdownTheme { linkUrl: (text) => muted(text), code: (text) => chalk.hex(colors.primary)(text), codeBlock: (text) => text, - codeBlockBorder: (text) => muted(text), + codeBlockBorder: (text) => dim(text), quote: (text) => dim(text), quoteBorder: (text) => dim(text), hr: (text) => border(text), @@ -45,13 +45,16 @@ export function createMarkdownTheme(colors: ColorPalette): MarkdownTheme { highlightCode: (code: string, lang?: string) => { const normalizedLang = lang?.trim().toLowerCase(); const language = - normalizedLang !== undefined && supportsLanguage(normalizedLang) ? normalizedLang : 'text'; - try { - const highlighted = highlight(code, { language, ignoreIllegals: true }); - return highlighted.split('\n'); - } catch { - return code.split('\n'); + normalizedLang !== undefined && supportsLanguage(normalizedLang) ? normalizedLang : undefined; + if (language) { + try { + const highlighted = highlight(code, { language, ignoreIllegals: true }); + return highlighted.split('\n'); + } catch { + // fall through to dim-styled fallback + } } + return code.split('\n').map((line) => dim(line)); }, }; } diff --git a/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts b/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts index 47d0836a..8a7c4695 100644 --- a/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts +++ b/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts @@ -28,17 +28,54 @@ describe('AssistantMessageComponent', () => { expect(visibleWidth(lines[1] ?? '')).toBe(8); }); - it('renders unknown markdown fence languages as plain text without stderr noise', () => { + it('renders unknown markdown fence languages without stderr noise', () => { const stderr = captureProcessWrite('stderr'); try { const theme = createMarkdownTheme(darkColors); - expect(theme.highlightCode?.('hello\nworld', 'abcxyz')).toEqual(['hello', 'world']); + const result = theme.highlightCode?.('hello\nworld', 'abcxyz') ?? []; + expect(result).toHaveLength(2); + expect(strip(result[0])).toBe('hello'); + expect(strip(result[1])).toBe('world'); expect(stderr.text()).not.toContain('Could not find the language'); } finally { stderr.restore(); } }); + it('renders headings without raw hash prefix', () => { + const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors); + component.updateContent('# Heading 1\n## Heading 2\n### Heading 3'); + const text = component.render(80).map(strip).join('\n'); + expect(text).toContain('Heading 1'); + expect(text).toContain('Heading 2'); + expect(text).toContain('Heading 3'); + expect(text).not.toContain('# Heading 1'); + expect(text).not.toContain('## Heading 2'); + expect(text).not.toContain('### Heading 3'); + }); + + it('renders bold text without raw asterisks', () => { + const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors); + component.updateContent('This is **bold** and __also bold__'); + const text = component.render(80).map(strip).join('\n'); + expect(text).toContain('bold'); + expect(text).toContain('also bold'); + expect(text).not.toContain('**bold**'); + expect(text).not.toContain('__also bold__'); + }); + + it('renders lists without raw dash markers', () => { + const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors); + component.updateContent('- item 1\n- item 2\n- item 3'); + const text = component.render(80).map(strip).join('\n'); + expect(text).toContain('item 1'); + expect(text).toContain('item 2'); + expect(text).toContain('item 3'); + expect(text).not.toContain('- item 1'); + expect(text).not.toContain('- item 2'); + expect(text).not.toContain('- item 3'); + }); + it('preserves literal hook result XML in normal assistant text', () => { const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors);