Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ public/vs
*.sw?

# Agent
.claude
.claude
/docs/superpowers
22 changes: 19 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
@custom-variant dark (&:is(.dark *));

:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
--font-sans: var(--font-jetbrains-mono, monospace);
font-family: var(--font-sans);
font-size: var(--font-size-base);
line-height: var(--density-line-height);
font-weight: 400;

font-synthesis: none;
Expand Down Expand Up @@ -48,6 +49,12 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--font-code: var(--font-jetbrains-mono, monospace);
--font-size-base: 16px;
--font-size-code: 14px;
--density-padding: 0.5rem;
--density-gap: 0.5rem;
--density-line-height: 1.5;
}

.dark {
Expand Down Expand Up @@ -117,6 +124,7 @@

@theme inline {
--font-sans: var(--font-sans);
--font-code: var(--font-code);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
Expand Down Expand Up @@ -170,6 +178,14 @@
}
}

code,
pre,
.font-mono,
[data-streamdown="code-block-body"] {
font-family: var(--font-code);
font-size: var(--font-size-code);
}

/* Streamdown code blocks: dark mode via shiki dual-theme CSS variables */
.dark [data-streamdown="code-block-body"] {
background-color: var(--shiki-dark-bg, var(--sdm-bg, transparent)) !important;
Expand Down
13 changes: 9 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { Metadata } from "next"
import "./globals.css"
import { JetBrains_Mono } from "next/font/google"
import { Inter, JetBrains_Mono } from "next/font/google"
import { NextIntlClientProvider } from "next-intl"
import { AppI18nProvider } from "@/components/i18n-provider"
import { getMessagesForLocale } from "@/i18n/messages"
import { resolveRequestLocale } from "@/i18n/resolve-request-locale"
import { ThemeProvider } from "@/components/theme-provider"
import { toIntlLocale } from "@/lib/i18n"
import { AppearanceProvider } from "@/lib/appearance/use-appearance"

const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
})
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-sans",
variable: "--font-jetbrains-mono",
})

export const metadata: Metadata = {
Expand All @@ -30,7 +35,7 @@ export default async function RootLayout({
return (
<html
lang={initialLocale}
className={jetbrainsMono.variable}
className={`${inter.variable} ${jetbrainsMono.variable}`}
suppressHydrationWarning
>
<body>
Expand All @@ -50,7 +55,7 @@ export default async function RootLayout({
enableSystem
disableTransitionOnChange
>
{children}
<AppearanceProvider>{children}</AppearanceProvider>
</ThemeProvider>
</AppI18nProvider>
</NextIntlClientProvider>
Expand Down
73 changes: 52 additions & 21 deletions src/components/ai-elements/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useAppearance } from "@/lib/appearance/use-appearance"
import { cn } from "@/lib/utils"
import { CheckIcon, CopyIcon } from "lucide-react"
import {
Expand Down Expand Up @@ -124,32 +125,40 @@ const highlighterCache = new Map<
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
>()

// Token cache
const MAX_TOKENS_CACHE = 200
const tokensCache = new Map<string, TokenizedCode>()

// Subscribers for async token updates
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>()

const getTokensCacheKey = (code: string, language: BundledLanguage) => {
const getTokensCacheKey = (
code: string,
language: BundledLanguage,
lightTheme: string,
darkTheme: string
) => {
const start = code.slice(0, 100)
const end = code.length > 100 ? code.slice(-100) : ""
return `${language}:${code.length}:${start}:${end}`
return `${language}:${lightTheme}:${darkTheme}:${code.length}:${start}:${end}`
}

const getHighlighter = (
language: BundledLanguage
language: BundledLanguage,
lightTheme: string,
darkTheme: string
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
const cached = highlighterCache.get(language)
const cacheKey = `${language}:${lightTheme}:${darkTheme}`
const cached = highlighterCache.get(cacheKey)
if (cached) {
return cached
}

const highlighterPromise = createHighlighter({
langs: [language],
themes: ["github-light", "github-dark"],
themes: [lightTheme as BundledTheme, darkTheme as BundledTheme],
})

highlighterCache.set(language, highlighterPromise)
highlighterCache.set(cacheKey, highlighterPromise)
return highlighterPromise
}

Expand All @@ -173,10 +182,17 @@ const createRawTokens = (code: string): TokenizedCode => ({
export const highlightCode = (
code: string,
language: BundledLanguage,
lightTheme: string,
darkTheme: string,
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
callback?: (result: TokenizedCode) => void
): TokenizedCode | null => {
const tokensCacheKey = getTokensCacheKey(code, language)
const tokensCacheKey = getTokensCacheKey(
code,
language,
lightTheme,
darkTheme
)

// Return cached result if available
const cached = tokensCache.get(tokensCacheKey)
Expand All @@ -193,7 +209,7 @@ export const highlightCode = (
}

// Start highlighting in background - fire-and-forget async pattern
getHighlighter(language)
getHighlighter(language, lightTheme, darkTheme)
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
.then((highlighter) => {
const availableLangs = highlighter.getLoadedLanguages()
Expand All @@ -202,8 +218,8 @@ export const highlightCode = (
const result = highlighter.codeToTokens(code, {
lang: langToUse,
themes: {
dark: "github-dark",
light: "github-light",
dark: darkTheme as BundledTheme,
light: lightTheme as BundledTheme,
},
})

Expand All @@ -213,8 +229,11 @@ export const highlightCode = (
tokens: result.tokens,
}

// Cache the result
tokensCache.set(tokensCacheKey, tokenized)
if (tokensCache.size > MAX_TOKENS_CACHE) {
const first = tokensCache.keys().next().value
if (first !== undefined) tokensCache.delete(first)
}

// Notify all subscribers
const subs = subscribers.get(tokensCacheKey)
Expand Down Expand Up @@ -383,40 +402,52 @@ export const CodeBlockContent = ({
language: BundledLanguage
showLineNumbers?: boolean
}) => {
const { settings } = useAppearance()
const lightTheme = settings.codeThemeLight
const darkTheme = settings.codeThemeDark

// Memoized raw tokens for immediate display
const rawTokens = useMemo(() => createRawTokens(code), [code])

// Synchronous cached-or-raw value, recomputed when code/language changes
const syncTokenized = useMemo(
() => highlightCode(code, language) ?? rawTokens,
[code, language, rawTokens]
() => highlightCode(code, language, lightTheme, darkTheme) ?? rawTokens,
[code, language, lightTheme, darkTheme, rawTokens]
)

// Async highlighted result, tagged with its source code/language
const [asyncState, setAsyncState] = useState<{
code: string
language: string
lightTheme: string
darkTheme: string
tokenized: TokenizedCode
} | null>(null)

useEffect(() => {
let cancelled = false

// Subscribe to async highlighting result
highlightCode(code, language, (result) => {
highlightCode(code, language, lightTheme, darkTheme, (result) => {
if (!cancelled) {
setAsyncState({ code, language, tokenized: result })
setAsyncState({
code,
language,
lightTheme,
darkTheme,
tokenized: result,
})
}
})

return () => {
cancelled = true
}
}, [code, language])
}, [code, language, lightTheme, darkTheme])

// Use async result only if it matches current code/language
const tokenized =
asyncState?.code === code && asyncState?.language === language
asyncState?.code === code &&
asyncState?.language === language &&
asyncState?.lightTheme === lightTheme &&
asyncState?.darkTheme === darkTheme
? asyncState.tokenized
: syncTokenized

Expand Down
Loading