Detailed changes
@@ -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"
+ }
}
}
@@ -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
@@ -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;
@@ -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 (
- <div className="app-container">
- {/* Conversations drawer */}
- <ConversationDrawer
- isOpen={drawerOpen}
- isCollapsed={drawerCollapsed}
- onClose={() => 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 */}
- <div className="main-content">
- <ChatInterface
- conversationId={currentConversationId}
- onOpenDrawer={() => setDrawerOpen(true)}
+ <WorkerPoolContextProvider
+ poolOptions={diffsPoolOptions}
+ highlighterOptions={diffsHighlighterOptions}
+ >
+ <div className="app-container">
+ {/* Conversations drawer */}
+ <ConversationDrawer
+ isOpen={drawerOpen}
+ isCollapsed={drawerCollapsed}
+ onClose={() => 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}
/>
- </div>
- {/* Command Palette */}
- <CommandPalette
- isOpen={commandPaletteOpen}
- onClose={() => 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)}
- />
-
- <ModelsModal
- isOpen={modelsModalOpen}
- onClose={() => setModelsModalOpen(false)}
- onModelsChanged={() => setModelsRefreshTrigger((prev) => prev + 1)}
- />
-
- {/* Backdrop for mobile drawer */}
- {drawerOpen && (
- <div className="backdrop hide-on-desktop" onClick={() => setDrawerOpen(false)} />
- )}
- </div>
+ {/* Main chat interface */}
+ <div className="main-content">
+ <ChatInterface
+ conversationId={currentConversationId}
+ onOpenDrawer={() => 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)}
+ />
+ </div>
+
+ {/* Command Palette */}
+ <CommandPalette
+ isOpen={commandPaletteOpen}
+ onClose={() => 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)}
+ />
+
+ <ModelsModal
+ isOpen={modelsModalOpen}
+ onClose={() => setModelsModalOpen(false)}
+ onModelsChanged={() => setModelsRefreshTrigger((prev) => prev + 1)}
+ />
+
+ {/* Backdrop for mobile drawer */}
+ {drawerOpen && (
+ <div className="backdrop hide-on-desktop" onClick={() => setDrawerOpen(false)} />
+ )}
+ </div>
+ </WorkerPoolContextProvider>
);
}
@@ -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;