From a45376d58569a1ba293156325e87849a098f8b77 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 15 Jun 2026 03:29:40 +0300 Subject: [PATCH] perf: migrate chat rendering to virtua (#1651) * refactor: migrate chat history virtualization to virtua * refactor: render loaded chat history directly * refactor: finish virtua migration * perf: defer tool body rendering * perf: queue deferred tool body mounts * perf: quiet and defer markdown file probes * perf: defer markdown code highlighting * perf: stabilize markdown plugin lists * perf: defer mermaid markdown rendering * perf: delay markdown file reference annotation * perf: attach markdown table listeners on demand * perf: trim markdown render overhead --- bun.lock | 8 +- packages/ui/package.json | 2 +- packages/ui/src/apps/MobileFilesSurface.tsx | 2 +- .../components/chat/MarkdownRendererImpl.tsx | 343 +++++++++++++----- .../ui/src/components/chat/MessageList.tsx | 261 +++++-------- .../chat/hooks/useChatTimelineController.ts | 127 +------ .../components/chat/lib/turns/constants.ts | 6 - .../components/chat/lib/turns/windowTurns.ts | 44 --- .../components/chat/message/MessageBody.tsx | 1 + .../chat/message/ToolOutputDialog.tsx | 1 + .../chat/message/parts/ToolPart.tsx | 93 +++-- .../chat/message/parts/UserTextPart.tsx | 3 +- .../message/parts/VirtualizedCodeBlock.tsx | 59 +-- .../components/sections/skills/SkillsPage.tsx | 1 + .../session/sidebar/SessionGroupSection.tsx | 86 +---- .../ui/src/components/ui/JsonTreeViewer.tsx | 54 +-- .../ui/src/components/ui/UpdateDialog.tsx | 4 +- .../ui/src/components/views/FilesView.tsx | 22 +- packages/ui/src/components/views/PlanView.tsx | 2 +- .../src/components/views/git/ChangesPanel.tsx | 87 ++--- .../views/git/PullRequestSection.tsx | 2 + .../src/bridge-localfs-proxy-runtime.test.js | 59 +++ .../src/bridge-localfs-proxy-runtime.ts | 21 ++ packages/web/server/lib/fs/routes.js | 4 + 24 files changed, 621 insertions(+), 671 deletions(-) create mode 100644 packages/vscode/src/bridge-localfs-proxy-runtime.test.js diff --git a/bun.lock b/bun.lock index 5cba7b78..b564331a 100644 --- a/bun.lock +++ b/bun.lock @@ -149,7 +149,6 @@ "@pierre/diffs": "1.3.0-beta.4", "@replit/codemirror-vim": "^6.3.0", "@simplewebauthn/browser": "13.3.0", - "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "@xenova/transformers": "^2.17.2", "@zumer/snapdom": "^2.12.0", @@ -183,6 +182,7 @@ "sonner": "^2.0.7", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", + "virtua": "0.49.1", "yaml": "^2.8.1", "zod": "^4.3.6", "zustand": "^5.0.8", @@ -1247,10 +1247,6 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.19", "", { "dependencies": { "@tanstack/virtual-core": "3.13.19" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ=="], - - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="], - "@textlint/ast-node-types": ["@textlint/ast-node-types@15.5.2", "", {}, "sha512-fCaOxoup5LIyBEo7R1oYWE7V4bSX0KQeHh66twon9e9usaLE3ijgF8QjYsR6joCssdeCHVd0wHm7ppsEyTr6vg=="], "@textlint/linter-formatter": ["@textlint/linter-formatter@15.5.2", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.5.2", "@textlint/resolver": "15.5.2", "@textlint/types": "15.5.2", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.17.23", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-jAw7jWM8+wU9cG6Uu31jGyD1B+PAVePCvnPKC/oov+2iBPKk3ao30zc/Itmi7FvXo4oPaL9PmzPPQhyniPVgVg=="], @@ -3223,6 +3219,8 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index bec5fa1d..46d5b776 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,7 +44,6 @@ "@pierre/diffs": "1.3.0-beta.4", "@replit/codemirror-vim": "^6.3.0", "@simplewebauthn/browser": "13.3.0", - "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "@xenova/transformers": "^2.17.2", "@zumer/snapdom": "^2.12.0", @@ -78,6 +77,7 @@ "sonner": "^2.0.7", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", + "virtua": "0.49.1", "yaml": "^2.8.1", "zod": "^4.3.6", "zustand": "^5.0.8" diff --git a/packages/ui/src/apps/MobileFilesSurface.tsx b/packages/ui/src/apps/MobileFilesSurface.tsx index f42d6378..e4080749 100644 --- a/packages/ui/src/apps/MobileFilesSurface.tsx +++ b/packages/ui/src/apps/MobileFilesSurface.tsx @@ -493,7 +493,7 @@ const MobileTextFile: React.FC<{ path: string; content: string }> = ({ path, con if (isMarkdownFile(path)) { return ( - + ); } diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx index 57167371..2209caa9 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx @@ -2,7 +2,7 @@ import React from 'react'; import 'katex/dist/katex.min.css'; import { renderMermaidASCII, renderMermaidSVG } from 'beautiful-mermaid'; import ReactMarkdown from 'react-markdown'; -import type { Components } from 'react-markdown'; +import type { Components, Options as ReactMarkdownOptions } from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; @@ -216,6 +216,10 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject(null); React.useEffect(() => { + if (!showMenu) { + return; + } + const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setShowMenu(false); @@ -223,7 +227,7 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject document.removeEventListener('mousedown', handleClickOutside); - }, []); + }, [showMenu]); const handleCopy = async (format: 'csv' | 'tsv' | 'markdown') => { const tableEl = tableRef.current?.querySelector('table'); @@ -308,6 +312,10 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject(null); React.useEffect(() => { + if (!showMenu) { + return; + } + const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setShowMenu(false); @@ -315,7 +323,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject document.removeEventListener('mousedown', handleClickOutside); - }, []); + }, [showMenu]); const handleDownload = (format: 'csv' | 'markdown') => { const tableEl = tableRef.current?.querySelector('table'); @@ -364,16 +372,35 @@ const TableWrapper: React.FC<{ children?: React.ReactNode; className?: string }> const tableRef = React.useRef(null); const { isMobile, isTablet } = useDeviceInfo(); const alwaysShowActions = isMobile || isTablet; + const [showActions, setShowActions] = React.useState(alwaysShowActions); + + React.useEffect(() => { + if (alwaysShowActions) { + setShowActions(true); + } + }, [alwaysShowActions]); return ( -
-
- - -
+
setShowActions(true)} + onMouseLeave={() => { + if (!alwaysShowActions) { + setShowActions(false); + } + }} + onFocusCapture={() => setShowActions(true)} + > + {showActions ? ( +
+ + +
+ ) : ( +