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 ( -
- {/* Conversations drawer */} - setDrawerOpen(false)} - onToggleCollapse={toggleDrawerCollapsed} - conversations={conversations} - currentConversationId={currentConversationId} - viewedConversation={viewedConversation} - onSelectConversation={selectConversation} - onNewConversation={startNewConversation} - onConversationArchived={handleConversationArchived} - onConversationUnarchived={handleConversationUnarchived} - onConversationRenamed={handleConversationRenamed} - subagentUpdate={subagentUpdate} - subagentStateUpdate={subagentStateUpdate} - /> - - {/* Main chat interface */} -
- setDrawerOpen(true)} + +
+ {/* Conversations drawer */} + setDrawerOpen(false)} + onToggleCollapse={toggleDrawerCollapsed} + conversations={conversations} + currentConversationId={currentConversationId} + viewedConversation={viewedConversation} + onSelectConversation={selectConversation} onNewConversation={startNewConversation} - currentConversation={currentConversation} - onConversationUpdate={updateConversation} - onConversationListUpdate={handleConversationListUpdate} - onConversationStateUpdate={handleConversationStateUpdate} - onFirstMessage={handleFirstMessage} - onContinueConversation={handleContinueConversation} - mostRecentCwd={mostRecentCwd} - isDrawerCollapsed={drawerCollapsed} - onToggleDrawerCollapse={toggleDrawerCollapsed} - openDiffViewerTrigger={diffViewerTrigger} - modelsRefreshTrigger={modelsRefreshTrigger} - onOpenModelsModal={() => setModelsModalOpen(true)} + onConversationArchived={handleConversationArchived} + onConversationUnarchived={handleConversationUnarchived} + onConversationRenamed={handleConversationRenamed} + subagentUpdate={subagentUpdate} + subagentStateUpdate={subagentStateUpdate} /> -
- {/* Command Palette */} - setCommandPaletteOpen(false)} - conversations={conversations} - onNewConversation={() => { - startNewConversation(); - setCommandPaletteOpen(false); - }} - onSelectConversation={(conversation) => { - selectConversation(conversation); - setCommandPaletteOpen(false); - }} - onOpenDiffViewer={() => { - setDiffViewerTrigger((prev) => prev + 1); - setCommandPaletteOpen(false); - }} - onOpenModelsModal={() => { - setModelsModalOpen(true); - setCommandPaletteOpen(false); - }} - hasCwd={!!(currentConversation?.cwd || mostRecentCwd)} - /> - - setModelsModalOpen(false)} - onModelsChanged={() => setModelsRefreshTrigger((prev) => prev + 1)} - /> - - {/* Backdrop for mobile drawer */} - {drawerOpen && ( -
setDrawerOpen(false)} /> - )} -
+ {/* Main chat interface */} +
+ setDrawerOpen(true)} + onNewConversation={startNewConversation} + currentConversation={currentConversation} + onConversationUpdate={updateConversation} + onConversationListUpdate={handleConversationListUpdate} + onConversationStateUpdate={handleConversationStateUpdate} + onFirstMessage={handleFirstMessage} + onContinueConversation={handleContinueConversation} + mostRecentCwd={mostRecentCwd} + isDrawerCollapsed={drawerCollapsed} + onToggleDrawerCollapse={toggleDrawerCollapsed} + openDiffViewerTrigger={diffViewerTrigger} + modelsRefreshTrigger={modelsRefreshTrigger} + onOpenModelsModal={() => setModelsModalOpen(true)} + /> +
+ + {/* Command Palette */} + setCommandPaletteOpen(false)} + conversations={conversations} + onNewConversation={() => { + startNewConversation(); + setCommandPaletteOpen(false); + }} + onSelectConversation={(conversation) => { + selectConversation(conversation); + setCommandPaletteOpen(false); + }} + onOpenDiffViewer={() => { + setDiffViewerTrigger((prev) => prev + 1); + setCommandPaletteOpen(false); + }} + onOpenModelsModal={() => { + setModelsModalOpen(true); + setCommandPaletteOpen(false); + }} + hasCwd={!!(currentConversation?.cwd || mostRecentCwd)} + /> + + setModelsModalOpen(false)} + onModelsChanged={() => setModelsRefreshTrigger((prev) => prev + 1)} + /> + + {/* Backdrop for mobile drawer */} + {drawerOpen && ( +
setDrawerOpen(false)} /> + )} +
+
); } diff --git a/ui/src/diffs-worker.ts b/ui/src/diffs-worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..99de8c35a5fe67996f8badf9d9746be436219da3 --- /dev/null +++ b/ui/src/diffs-worker.ts @@ -0,0 +1,11 @@ +// Web Worker for @pierre/diffs syntax highlighting +// This offloads tokenization to background threads for better performance +// Note: This file is built as IIFE and runs in a Worker context +// +// We import and reference to prevent tree-shaking - the worker.js file +// sets up self.addEventListener("message", ...) which is a side effect +import * as diffsWorker from "@pierre/diffs/worker/worker.js"; + +// Prevent tree-shaking by referencing the import +// The worker module registers message handlers as a side effect +(globalThis as unknown as { __diffsWorker: unknown }).__diffsWorker = diffsWorker;