diff --git a/ui/package.json b/ui/package.json index 04a80b3fc4f5188f76cfe9aed68c2f4757adac33..82f42424c3ad95734491d5426b8d9b3bf17010cd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,7 +20,7 @@ "test:e2e:debug": "pnpm run build && playwright test --debug" }, "dependencies": { - "@pierre/diffs": "^1.0.9", + "@pierre/diffs": "^1.0.10", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", @@ -49,6 +49,12 @@ "pnpm": { "onlyBuiltDependencies": [ "esbuild" - ] + ], + "overrides": { + "shiki": "^3.22.0", + "@shikijs/core": "^3.22.0", + "@shikijs/engine-javascript": "^3.22.0", + "@shikijs/transformers": "^3.22.0" + } } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 10f590921c9713afeccd7777a9fee5bc0f95d9bf..7989a44cade6f372118de8a55dfc1b232f4e1f62 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -4,13 +4,19 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + shiki: ^3.22.0 + '@shikijs/core': ^3.22.0 + '@shikijs/engine-javascript': ^3.22.0 + '@shikijs/transformers': ^3.22.0 + importers: .: dependencies: '@pierre/diffs': - specifier: ^1.0.9 - version: 1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.0.10 + version: 1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -429,8 +435,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@pierre/diffs@1.0.9': - resolution: {integrity: sha512-PiRhcAzz0yuifRTe2DmTsKOHQgGf1qgh5W9OVO3/c+DONrjo1PM5tquOxndCAZDTdPxwzGuWc6jPUaDrfu6nDg==} + '@pierre/diffs@1.0.10': + resolution: {integrity: sha512-ahkpfS30NfaB+PBxnf0/Mc20ySBRTQmM28a7Ojpd0UZixmTyhGhJfBFjvmhX8dSzR22lB3h3OIMMxpB4yYTIOQ==} peerDependencies: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 @@ -440,26 +446,26 @@ packages: engines: {node: '>=18'} hasBin: true - '@shikijs/core@3.21.0': - resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} + '@shikijs/core@3.22.0': + resolution: {integrity: sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==} - '@shikijs/engine-javascript@3.21.0': - resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} + '@shikijs/engine-javascript@3.22.0': + resolution: {integrity: sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==} - '@shikijs/engine-oniguruma@3.21.0': - resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + '@shikijs/engine-oniguruma@3.22.0': + resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==} - '@shikijs/langs@3.21.0': - resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + '@shikijs/langs@3.22.0': + resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==} - '@shikijs/themes@3.21.0': - resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + '@shikijs/themes@3.22.0': + resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==} - '@shikijs/transformers@3.21.0': - resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==} + '@shikijs/transformers@3.22.0': + resolution: {integrity: sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw==} - '@shikijs/types@3.21.0': - resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + '@shikijs/types@3.22.0': + resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -720,8 +726,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@8.0.2: - resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} doctrine@2.1.0: @@ -1381,8 +1387,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@3.21.0: - resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + shiki@3.22.0: + resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==} side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} @@ -1766,54 +1772,54 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@pierre/diffs@1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@pierre/diffs@1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@shikijs/core': 3.21.0 - '@shikijs/engine-javascript': 3.21.0 - '@shikijs/transformers': 3.21.0 - diff: 8.0.2 + '@shikijs/core': 3.22.0 + '@shikijs/engine-javascript': 3.22.0 + '@shikijs/transformers': 3.22.0 + diff: 8.0.3 hast-util-to-html: 9.0.5 lru_map: 0.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - shiki: 3.21.0 + shiki: 3.22.0 '@playwright/test@1.57.0': dependencies: playwright: 1.57.0 - '@shikijs/core@3.21.0': + '@shikijs/core@3.22.0': dependencies: - '@shikijs/types': 3.21.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.21.0': + '@shikijs/engine-javascript@3.22.0': dependencies: - '@shikijs/types': 3.21.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.4 - '@shikijs/engine-oniguruma@3.21.0': + '@shikijs/engine-oniguruma@3.22.0': dependencies: - '@shikijs/types': 3.21.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.21.0': + '@shikijs/langs@3.22.0': dependencies: - '@shikijs/types': 3.21.0 + '@shikijs/types': 3.22.0 - '@shikijs/themes@3.21.0': + '@shikijs/themes@3.22.0': dependencies: - '@shikijs/types': 3.21.0 + '@shikijs/types': 3.22.0 - '@shikijs/transformers@3.21.0': + '@shikijs/transformers@3.22.0': dependencies: - '@shikijs/core': 3.21.0 - '@shikijs/types': 3.21.0 + '@shikijs/core': 3.22.0 + '@shikijs/types': 3.22.0 - '@shikijs/types@3.21.0': + '@shikijs/types@3.22.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -2133,7 +2139,7 @@ snapshots: dependencies: dequal: 2.0.3 - diff@8.0.2: {} + diff@8.0.3: {} doctrine@2.1.0: dependencies: @@ -2995,14 +3001,14 @@ snapshots: shebang-regex@3.0.0: {} - shiki@3.21.0: + shiki@3.22.0: dependencies: - '@shikijs/core': 3.21.0 - '@shikijs/engine-javascript': 3.21.0 - '@shikijs/engine-oniguruma': 3.21.0 - '@shikijs/langs': 3.21.0 - '@shikijs/themes': 3.21.0 - '@shikijs/types': 3.21.0 + '@shikijs/core': 3.22.0 + '@shikijs/engine-javascript': 3.22.0 + '@shikijs/engine-oniguruma': 3.22.0 + '@shikijs/langs': 3.22.0 + '@shikijs/themes': 3.22.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 diff --git a/ui/scripts/build.js b/ui/scripts/build.js index e96c47d32dee3c81fd38ab3553900842786f47d5..5c6882bbfcebfe860edee82936daa5029ee106eb 100644 --- a/ui/scripts/build.js +++ b/ui/scripts/build.js @@ -31,6 +31,17 @@ async function build() { sourcemap: true, }); + // Build @pierre/diffs worker for syntax highlighting (IIFE format for web worker) + log('Building diffs worker...'); + await esbuild.build({ + entryPoints: ['src/diffs-worker.ts'], + bundle: true, + outfile: 'dist/diffs-worker.js', + format: 'iife', + minify: isProd, + sourcemap: true, + }); + // Build Monaco editor as a separate chunk (JS + CSS) log('Building Monaco editor bundle...'); await esbuild.build({ @@ -105,7 +116,7 @@ async function build() { // Generate gzip versions of large files and remove originals to reduce binary size // The server will decompress on-the-fly for the rare clients that don't support gzip log('\nGenerating gzip compressed files...'); - const filesToCompress = ['monaco-editor.js', 'editor.worker.js', 'main.js', 'monaco-editor.css', 'styles.css', 'main.css']; + const filesToCompress = ['monaco-editor.js', 'editor.worker.js', 'diffs-worker.js', 'main.js', 'monaco-editor.css', 'styles.css', 'main.css']; const checksums = {}; let totalOrigSize = 0; let totalGzSize = 0; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d0b0ce711fcf8ae4777064c7c6db8c8025504e5a..ac000d4977fc925f093a53d5a2b55827aff2123b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; +import { WorkerPoolContextProvider } from "@pierre/diffs/react"; +import type { SupportedLanguages } from "@pierre/diffs"; import ChatInterface from "./components/ChatInterface"; import ConversationDrawer from "./components/ConversationDrawer"; import CommandPalette from "./components/CommandPalette"; @@ -6,6 +8,44 @@ import ModelsModal from "./components/ModelsModal"; import { Conversation, ConversationWithState, ConversationListUpdate } from "./types"; import { api } from "./services/api"; +// Worker pool configuration for @pierre/diffs syntax highlighting +// Workers run tokenization off the main thread for better performance with large diffs +const diffsPoolOptions = { + workerFactory: () => new Worker("/diffs-worker.js"), +}; + +// Languages to preload in the highlighter (matches PatchTool.tsx langMap) +const diffsHighlighterOptions = { + langs: [ + "typescript", + "tsx", + "javascript", + "jsx", + "python", + "ruby", + "go", + "rust", + "java", + "c", + "cpp", + "csharp", + "php", + "swift", + "kotlin", + "scala", + "bash", + "sql", + "html", + "css", + "scss", + "json", + "xml", + "yaml", + "toml", + "markdown", + ] as SupportedLanguages[], +}; + // Check if a slug is a generated ID (format: cXXXX where X is alphanumeric) function isGeneratedId(slug: string | null): boolean { if (!slug) return true; @@ -387,81 +427,86 @@ function App() { }; return ( -