Skip to content
Merged
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
31 changes: 21 additions & 10 deletions apps/claude-sdk-cli/src/renderConversation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DIM, RESET } from '@shellicar/claude-core/ansi';
import { wrapLine } from '@shellicar/claude-core/reflow';
import { highlight } from 'cli-highlight';
import { highlight, supportsLanguage } from 'cli-highlight';
import type { Block, ConversationState } from './ConversationState.js';

const FILL = '\u2500';
Expand All @@ -27,6 +27,24 @@ const CONTENT_INDENT = ' ';

const CODE_FENCE_RE = /```(\w*)\n([\s\S]*?)```/g;

// Some fence language identifiers don't match highlight.js names.
// Map them so we get proper syntax colouring instead of a silent fallback.
const LANGUAGE_ALIASES: Record<string, string> = {
jsonl: 'json',
};

function getHighlighted(code: string, lang: string): string[] {
const hlLang = LANGUAGE_ALIASES[lang] ?? lang;
if (!supportsLanguage(hlLang)) {
return code.split('\n');
}
try {
return highlight(code, { language: hlLang, ignoreIllegals: true }).split('\n');
} catch {
return code.split('\n');
}
}

function renderBlockContent(content: string, cols: number): string[] {
const result: string[] = [];
let lastIndex = 0;
Expand All @@ -46,15 +64,8 @@ function renderBlockContent(content: string, cols: number): string[] {
const lang = match[1] || 'plaintext';
const code = (match[2] ?? '').trimEnd();
result.push(`${CONTENT_INDENT}\`\`\`${lang}`);
try {
const highlighted = highlight(code, { language: lang, ignoreIllegals: true });
for (const line of highlighted.split('\n')) {
result.push(CONTENT_INDENT + line);
}
} catch {
for (const line of code.split('\n')) {
result.push(CONTENT_INDENT + line);
}
for (const line of getHighlighted(code, lang)) {
result.push(CONTENT_INDENT + line);
}
result.push(`${CONTENT_INDENT}\`\`\``);
lastIndex = match.index + match[0].length;
Expand Down
27 changes: 27 additions & 0 deletions apps/claude-sdk-cli/test/renderConversation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,30 @@ describe('buildDivider', () => {
expect(actual).toBe(true);
});
});

describe('renderConversation — code fence highlighting', () => {
it('renders code from an unknown language without warning (plain fallback)', () => {
const state = new ConversationState();
state.addBlocks([{ type: 'response', content: '```unknownxyz\nfoo bar\n```' }]);
const lines = renderConversation(state, 80).map(stripAnsi);
const actual = lines.some((l) => l.includes('foo bar'));
expect(actual).toBe(true);
});

it('preserves the original fence label even when an alias is used for highlighting', () => {
const state = new ConversationState();
state.addBlocks([{ type: 'response', content: '```jsonl\n{"key": 1}\n```' }]);
const lines = renderConversation(state, 80).map(stripAnsi);
// Fence header should show the original language name, not the alias
const actual = lines.some((l) => l.includes('```jsonl'));
expect(actual).toBe(true);
});

it('renders jsonl code content', () => {
const state = new ConversationState();
state.addBlocks([{ type: 'response', content: '```jsonl\n{"key": 1}\n```' }]);
const lines = renderConversation(state, 80).map(stripAnsi);
const actual = lines.some((l) => l.includes('"key"'));
expect(actual).toBe(true);
});
});
Loading