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..b770f900 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,25 +19,39 @@ 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 { this.showBullet = show; } + applyTheme(markdownTheme: MarkdownTheme, colors: ColorPalette): void { + this.markdownTheme = markdownTheme; + this.bulletColor = colors.roleAssistant; + this.defaultTextStyle = { color: (text) => chalk.hex(colors.text)(text) }; + if (this.lastText) { + this.contentContainer.clear(); + this.contentContainer.addChild( + new Markdown(this.lastText.trim(), 0, 0, this.markdownTheme, this.defaultTextStyle), + ); + } + } + updateContent(text: string): void { const displayText = text; if (displayText === this.lastText) return; 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/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 68258aaa..96386933 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -106,7 +106,7 @@ import { type TUIStartupState, } from './types'; import { createTUIState, type TUIState } from './tui-state'; -import { isExpandable, isPlanExpandable } from './utils/component-capabilities'; +import { isExpandable, isPlanExpandable, isThemeAware } from './utils/component-capabilities'; import { isDeadTerminalError } from './utils/dead-terminal'; import { formatErrorMessage } from './utils/event-payload'; import { ImageAttachmentStore, type ImageAttachment } from './utils/image-attachment-store'; @@ -1486,6 +1486,11 @@ export class KimiTUI { this.state.theme.styles = nextTheme.styles; this.state.theme.markdownTheme = nextTheme.markdownTheme; this.setAppState({ theme }); + for (const child of this.state.transcriptContainer.children) { + if (isThemeAware(child)) { + child.applyTheme(this.state.theme.markdownTheme, this.state.theme.colors); + } + } this.updateEditorBorderHighlight(); this.state.ui.requestRender(true); } 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/src/tui/utils/component-capabilities.ts b/apps/kimi-code/src/tui/utils/component-capabilities.ts index 5f1f0ba9..dd4672f4 100644 --- a/apps/kimi-code/src/tui/utils/component-capabilities.ts +++ b/apps/kimi-code/src/tui/utils/component-capabilities.ts @@ -1,3 +1,7 @@ +import type { MarkdownTheme } from '@earendil-works/pi-tui'; + +import type { ColorPalette } from '#/tui/theme/colors'; + export interface Expandable { setExpanded(expanded: boolean): void; } @@ -12,6 +16,19 @@ export interface Disposable { dispose(): void; } +export interface ThemeAwareComponent { + applyTheme(markdownTheme: MarkdownTheme, colors: ColorPalette): void; +} + +export function isThemeAware(obj: unknown): obj is ThemeAwareComponent { + return ( + typeof obj === 'object' && + obj !== null && + 'applyTheme' in obj && + typeof (obj as ThemeAwareComponent).applyTheme === 'function' + ); +} + export function isExpandable(obj: unknown): obj is Expandable { return ( typeof obj === 'object' && 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);