Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/markdown-theme-rendering-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Apply text color to plain markdown paragraphs and dim code blocks with unknown fence languages.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 {
Expand All @@ -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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate assistant markdown on theme changes

When an assistant message has already rendered, this Markdown instance keeps its cached output and pi-tui also caches the default style prefix derived from this new defaultTextStyle; checked applyTheme in apps/kimi-code/src/tui/kimi-tui.ts, and it only mutates state.theme.colors plus requests a redraw, without invalidating/recreating transcript children. In the theme-switch path, existing assistant messages with plain or inline-formatted text therefore keep the old ANSI foreground color until their content changes or the component is rebuilt. Please ensure theme changes invalidate/recreate these Markdown instances (and their default style prefix) for existing assistant transcript entries.

Useful? React with 👍 / 👎.

}
}

Expand Down
17 changes: 10 additions & 7 deletions apps/kimi-code/src/tui/theme/pi-tui-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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));
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down