Ship desktop code blocks that render reliably in the macOS release build

The desktop app was still falling back to raw text in release builds for some
fenced code blocks, which left bash snippets visually wrong and too heavy for
chat use. This change switches the Shiki path to the JavaScript regex engine,
restores tighter code-block defaults for chat, and bumps the desktop app
version to 0.1.4 for the release artifacts.

Constraint: The packaged Tauri app must render markdown code blocks consistently without relying on WebView WASM behavior
Constraint: Chat code blocks should stay compact and should not show line numbers unless a caller explicitly requests them
Rejected: Keep tuning fallback-only CSS | did not address the packaged highlighter path failing to initialize
Rejected: Leave line numbers on by default | too noisy for assistant replies and bash snippets
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: If code blocks regress again, inspect the highlighter engine path before adjusting chat spacing CSS
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test CodeViewer.test.tsx MarkdownRenderer.test.tsx Sidebar.test.tsx
Tested: cd desktop && bun run build:macos-arm64
Not-tested: Windows packaged build after the engine switch
This commit is contained in:
程序员阿江(Relakkes) 2026-04-20 18:39:11 +08:00
parent 03112f7152
commit c70da2459d
7 changed files with 44 additions and 21 deletions

View File

@ -1,7 +1,7 @@
{
"name": "claude-code-desktop",
"private": true,
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -323,7 +323,7 @@ dependencies = [
[[package]]
name = "claude-code-desktop"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"serde",
"serde_json",

View File

@ -1,6 +1,6 @@
[package]
name = "claude-code-desktop"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
[lib]

View File

@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
"productName": "Claude Code Haha",
"version": "0.1.3",
"version": "0.1.4",
"identifier": "com.claude-code-haha.desktop",
"build": {
"frontendDist": "../dist",

View File

@ -2,18 +2,27 @@ import { render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { CodeViewer } from './CodeViewer'
const mockShikiState = vi.hoisted(() => ({
lastProps: null as Record<string, unknown> | null,
engine: { kind: 'js-regex-engine' as const },
}))
vi.mock('react-shiki', () => ({
ShikiHighlighter: ({ children }: { children: string }) => (
<div data-testid="shiki-container">
<code>{children}</code>
</div>
),
createJavaScriptRegexEngine: () => mockShikiState.engine,
ShikiHighlighter: (props: { children: string } & Record<string, unknown>) => {
mockShikiState.lastProps = props
return (
<div data-testid="shiki-container">
<code>{props.children}</code>
</div>
)
},
}))
describe('CodeViewer', () => {
it('keeps the same inner padding for highlighted code content', async () => {
const { container } = render(
<CodeViewer code={'cd testb\nnpm run dev'} language="bash" />,
<CodeViewer code={'cd testb\nnpm run dev'} language="bash" showLineNumbers />,
)
await waitFor(() => {
@ -23,5 +32,9 @@ describe('CodeViewer', () => {
const contentWrapper = container.querySelector('[data-code-viewer-content]') as HTMLElement | null
expect(contentWrapper).toBeTruthy()
expect(contentWrapper?.style.padding).toBe('0.5rem 12px')
const codeArea = container.querySelector('.code-viewer-area') as HTMLElement | null
expect(codeArea?.getAttribute('data-has-line-numbers')).toBe('true')
expect(mockShikiState.lastProps?.engine).toBe(mockShikiState.engine)
})
})

View File

@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react'
import { ShikiHighlighter } from 'react-shiki'
import { ShikiHighlighter, createJavaScriptRegexEngine } from 'react-shiki'
import 'react-shiki/css'
import { CopyButton } from '../shared/CopyButton'
@ -46,6 +46,8 @@ const warmCodeTheme = {
}
const CODE_AREA_PADDING = '0.5rem 12px'
const CODE_LINE_HEIGHT = 1.3
const shikiEngine = createJavaScriptRegexEngine({ forgiving: true })
/**
* Wraps ShikiHighlighter with a plain-text fallback so the code area
@ -76,7 +78,11 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
}, [code, language])
return (
<div ref={containerRef} className="code-viewer-area relative max-h-[420px] overflow-auto bg-[var(--color-code-bg)]">
<div
ref={containerRef}
data-has-line-numbers={showLineNumbers ? 'true' : 'false'}
className="code-viewer-area relative max-h-[420px] overflow-auto bg-[var(--color-code-bg)]"
>
{/* Plain-text fallback shown until Shiki finishes highlighting */}
{!loaded && (
<pre
@ -85,7 +91,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
padding: CODE_AREA_PADDING,
fontFamily: 'var(--font-mono)',
fontSize: '12px',
lineHeight: '1.45',
lineHeight: String(CODE_LINE_HEIGHT),
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: 'var(--color-code-fg)',
@ -111,6 +117,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
<ShikiHighlighter
language={language || 'text'}
theme={warmCodeTheme}
engine={shikiEngine}
showLineNumbers={showLineNumbers}
showLanguage={false}
addDefaultStyles={false}
@ -118,7 +125,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
margin: 0,
fontFamily: 'var(--font-mono)',
fontSize: '12px',
lineHeight: '1.45',
lineHeight: String(CODE_LINE_HEIGHT),
}}
>
{code}
@ -128,7 +135,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
)
}
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = true }: Props) {
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = false }: Props) {
const [expanded, setExpanded] = useState(false)
const allLines = code.split('\n')

View File

@ -70,24 +70,27 @@
/* ─── Shiki code viewer ───────────────────────────────────────── */
.code-viewer-area .line {
display: inline-block;
width: 100%;
padding: 1px 12px;
display: block;
padding: 0 12px;
}
.code-viewer-area .has-line-numbers .line {
padding-left: 0;
padding-left: 12px;
}
.code-viewer-area .line:hover {
background: rgba(0, 0, 0, 0.03);
}
.code-viewer-area .line-numbers::before {
margin-right: 1.5ch;
}
.code-viewer-area .has-line-numbers {
--line-numbers-foreground: rgba(135, 115, 109, 0.5);
--line-numbers-width: 2.5ch;
--line-numbers-padding-left: 1ch;
--line-numbers-padding-right: 1.5ch;
--line-numbers-padding-left: 0;
--line-numbers-padding-right: 0;
--line-numbers-font-size: 11px;
}