mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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:
parent
03112f7152
commit
c70da2459d
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-code-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
2
desktop/src-tauri/Cargo.lock
generated
2
desktop/src-tauri/Cargo.lock
generated
@ -323,7 +323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "claude-code-desktop"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "claude-code-desktop"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user