From c70da2459d00b6e8338865cc6822fa972c617bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 20 Apr 2026 18:39:11 +0800 Subject: [PATCH] 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 --- desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- .../src/components/chat/CodeViewer.test.tsx | 25 ++++++++++++++----- desktop/src/components/chat/CodeViewer.tsx | 17 +++++++++---- desktop/src/theme/globals.css | 15 ++++++----- 7 files changed, 44 insertions(+), 21 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 25557b6b..556b58fe 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "claude-code-desktop", "private": true, - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "dev": "vite", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 956b476b..31b5b306 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -323,7 +323,7 @@ dependencies = [ [[package]] name = "claude-code-desktop" -version = "0.1.3" +version = "0.1.4" dependencies = [ "serde", "serde_json", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index ef390f63..8ce03af1 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "claude-code-desktop" -version = "0.1.3" +version = "0.1.4" edition = "2021" [lib] diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 861fb7fe..5cfd8062 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -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", diff --git a/desktop/src/components/chat/CodeViewer.test.tsx b/desktop/src/components/chat/CodeViewer.test.tsx index 9f654f6b..90b602e4 100644 --- a/desktop/src/components/chat/CodeViewer.test.tsx +++ b/desktop/src/components/chat/CodeViewer.test.tsx @@ -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 | null, + engine: { kind: 'js-regex-engine' as const }, +})) + vi.mock('react-shiki', () => ({ - ShikiHighlighter: ({ children }: { children: string }) => ( -
- {children} -
- ), + createJavaScriptRegexEngine: () => mockShikiState.engine, + ShikiHighlighter: (props: { children: string } & Record) => { + mockShikiState.lastProps = props + return ( +
+ {props.children} +
+ ) + }, })) describe('CodeViewer', () => { it('keeps the same inner padding for highlighted code content', async () => { const { container } = render( - , + , ) 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) }) }) diff --git a/desktop/src/components/chat/CodeViewer.tsx b/desktop/src/components/chat/CodeViewer.tsx index 4b337cde..a6cb8726 100644 --- a/desktop/src/components/chat/CodeViewer.tsx +++ b/desktop/src/components/chat/CodeViewer.tsx @@ -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 ( -
+
{/* Plain-text fallback shown until Shiki finishes highlighting */} {!loaded && (
           {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')
diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css
index a24d6125..eeb72334 100644
--- a/desktop/src/theme/globals.css
+++ b/desktop/src/theme/globals.css
@@ -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;
 }