Fix syntax highlighting by upgrading to Shiki v3 with all languages

- Upgrade shiki from ^1.0.0 to ^3.13.0
- Use shiki/bundle/full with all bundled languages (200+)
- Change getHighlighter to createHighlighter (v3 API)
- Fix CodeBlockInline to track reactive dependencies (theme, code, language)
- Add markdown code block detection in tool call outputs
- Render tool outputs with Markdown component when they contain code blocks
- Support syntax highlighting for bash, webfetch, and default tool outputs
This commit is contained in:
Shantur Rathore
2025-10-23 21:38:16 +01:00
parent 4c98a3df06
commit 40cb17d0eb
4 changed files with 44 additions and 5 deletions

View File

@@ -21,7 +21,7 @@
"electron": "38.4.0", "electron": "38.4.0",
"lucide-solid": "^0.300.0", "lucide-solid": "^0.300.0",
"marked": "^12.0.0", "marked": "^12.0.0",
"shiki": "^1.0.0", "shiki": "^3.13.0",
"solid-js": "^1.8.0" "solid-js": "^1.8.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,5 @@
import { createSignal, onMount, Show, createEffect } from "solid-js" import { createSignal, onMount, Show, createEffect } from "solid-js"
import type { Highlighter } from "shiki" import type { Highlighter } from "shiki/bundle/full"
import { useTheme } from "../lib/theme" import { useTheme } from "../lib/theme"
import { getSharedHighlighter, escapeHtml } from "../lib/markdown" import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
@@ -23,6 +23,9 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
createEffect(() => { createEffect(() => {
if (ready()) { if (ready()) {
isDark()
props.code
props.language
updateHighlight() updateHighlight()
} }
}) })

View File

@@ -1,6 +1,8 @@
import { createSignal, Show, For, createEffect } from "solid-js" import { createSignal, Show, For, createEffect } from "solid-js"
import { isToolCallExpanded, toggleToolCallExpanded } from "../stores/tool-call-state" import { isToolCallExpanded, toggleToolCallExpanded } from "../stores/tool-call-state"
import { CodeBlockInline } from "./code-block-inline" import { CodeBlockInline } from "./code-block-inline"
import { Markdown } from "./markdown"
import { useTheme } from "../lib/theme"
interface ToolCallProps { interface ToolCallProps {
toolCall: any toolCall: any
@@ -96,7 +98,12 @@ function getLanguageFromPath(path: string): string | undefined {
return ext ? langMap[ext] : undefined return ext ? langMap[ext] : undefined
} }
function hasMarkdownCodeBlocks(text: string): boolean {
return /```[\s\S]*?```/.test(text)
}
export default function ToolCall(props: ToolCallProps) { export default function ToolCall(props: ToolCallProps) {
const { isDark } = useTheme()
const toolCallId = () => props.toolCallId || props.toolCall?.id || "" const toolCallId = () => props.toolCallId || props.toolCall?.id || ""
const expanded = () => isToolCallExpanded(toolCallId()) const expanded = () => isToolCallExpanded(toolCallId())
@@ -345,6 +352,17 @@ export default function ToolCall(props: ToolCallProps) {
if (input.command) { if (input.command) {
const fullOutput = `$ ${input.command}${output ? "\n" + output : ""}` const fullOutput = `$ ${input.command}${output ? "\n" + output : ""}`
if (output && hasMarkdownCodeBlocks(output)) {
return (
<div class="tool-call-bash">
<div class="message-text">
<Markdown content={fullOutput} isDark={isDark()} />
</div>
</div>
)
}
return ( return (
<div class="tool-call-bash"> <div class="tool-call-bash">
<CodeBlockInline code={fullOutput} language="bash" /> <CodeBlockInline code={fullOutput} language="bash" />
@@ -362,6 +380,15 @@ export default function ToolCall(props: ToolCallProps) {
if (output) { if (output) {
const lines = output.split("\n") const lines = output.split("\n")
const truncated = lines.slice(0, 10).join("\n") const truncated = lines.slice(0, 10).join("\n")
if (hasMarkdownCodeBlocks(truncated)) {
return (
<div class="message-text">
<Markdown content={truncated} isDark={isDark()} />
</div>
)
}
return <CodeBlockInline code={truncated} language="markdown" /> return <CodeBlockInline code={truncated} language="markdown" />
} }
@@ -448,6 +475,15 @@ export default function ToolCall(props: ToolCallProps) {
if (output) { if (output) {
const lines = output.split("\n") const lines = output.split("\n")
const truncated = lines.slice(0, 10).join("\n") const truncated = lines.slice(0, 10).join("\n")
if (hasMarkdownCodeBlocks(truncated)) {
return (
<div class="message-text">
<Markdown content={truncated} isDark={isDark()} />
</div>
)
}
return <CodeBlockInline code={truncated} /> return <CodeBlockInline code={truncated} />
} }

View File

@@ -1,5 +1,5 @@
import { marked } from "marked" import { marked } from "marked"
import { getHighlighter, type Highlighter } from "shiki" import { createHighlighter, type Highlighter, bundledLanguages } from "shiki/bundle/full"
let highlighter: Highlighter | null = null let highlighter: Highlighter | null = null
let highlighterPromise: Promise<Highlighter> | null = null let highlighterPromise: Promise<Highlighter> | null = null
@@ -15,9 +15,9 @@ async function getOrCreateHighlighter() {
return highlighterPromise return highlighterPromise
} }
highlighterPromise = getHighlighter({ highlighterPromise = createHighlighter({
themes: ["github-light", "github-dark"], themes: ["github-light", "github-dark"],
langs: [], langs: Object.keys(bundledLanguages),
}) })
highlighter = await highlighterPromise highlighter = await highlighterPromise