From 60dea43cc32bb56756514a0ec475ed616122d06d Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 17 Dec 2025 16:16:28 +0200 Subject: [PATCH] feat: optimize diff loading performance with background pre-fetching --- CHANGELOG.md | 2 + packages/ui/src/components/views/DiffView.tsx | 193 +++++------------- .../ui/src/contexts/DiffWorkerProvider.tsx | 160 ++++++++++++++- packages/ui/src/stores/useGitStore.ts | 136 +++++++++++- packages/web/public/favicon.png | Bin 0 -> 2158 bytes packages/web/public/vite.svg | 1 - 6 files changed, 346 insertions(+), 146 deletions(-) create mode 100644 packages/web/public/favicon.png delete mode 100644 packages/web/public/vite.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ceaa28..069c4441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. - Added image preview support in Diff tab (shows original/modified images instead of base64 code) - Improved diff view visuals and alligned style among different widgets +- Optimized git polling and background diff+syntax pre-warm for instant Diff tab open +- Optomized reloading unaffected diffs ## [1.2.2] - 2025-12-17 diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index e8a663ec..07e457e1 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -235,6 +235,45 @@ const SingleDiffViewer = React.memo(({ ); }); +interface DiffViewerEntryProps { + directory: string; + filePath: string; + isVisible: boolean; + renderSideBySide: boolean; + wrapLines: boolean; +} + +const DiffViewerEntry = React.memo(({ + directory, + filePath, + isVisible, + renderSideBySide, + wrapLines, +}) => { + const cachedDiff = useGitStore( + React.useCallback((state) => { + return state.directories.get(directory)?.diffCache.get(filePath) ?? null; + }, [directory, filePath]) + ); + + const diffData = React.useMemo(() => { + if (!cachedDiff) return null; + return { original: cachedDiff.original, modified: cachedDiff.modified }; + }, [cachedDiff?.original, cachedDiff?.modified]); + + if (!diffData) return null; + + return ( + + ); +}); + const useEffectiveDirectory = () => { const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore(); const { currentDirectory: fallbackDirectory } = useDirectoryStore(); @@ -254,11 +293,9 @@ export const DiffView: React.FC = () => { const isGitRepo = useIsGitRepo(effectiveDirectory ?? null); const status = useGitStatus(effectiveDirectory ?? null); const isLoadingStatus = useGitStore((state) => state.isLoadingStatus); - const { setActiveDirectory, fetchStatus, setDiff } = useGitStore(); - + const { setActiveDirectory, fetchStatus } = useGitStore(); + const [selectedFile, setSelectedFile] = React.useState(null); - const [allDiffs, setAllDiffs] = React.useState>(new Map()); - const [loadingFiles, setLoadingFiles] = React.useState>(new Set()); const pendingDiffFile = useUIStore((state) => state.pendingDiffFile); const setPendingDiffFile = useUIStore((state) => state.setPendingDiffFile); @@ -267,10 +304,6 @@ export const DiffView: React.FC = () => { const setDiffFileLayout = useUIStore((state) => state.setDiffFileLayout); const diffWrapLines = useUIStore((state) => state.diffWrapLines); const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines); - const lastStatusChange = useGitStore(React.useCallback((state) => { - if (!effectiveDirectory) return 0; - return state.directories.get(effectiveDirectory)?.lastStatusChange ?? 0; - }, [effectiveDirectory])); const changedFiles: FileEntry[] = React.useMemo(() => { if (!status?.files) return []; @@ -352,142 +385,30 @@ export const DiffView: React.FC = () => { } }, [changedFiles, selectedFile]); - // PRE-FETCH ALL DIFFS when changedFiles changes - React.useEffect(() => { - if (!effectiveDirectory || changedFiles.length === 0) return; - - const fetchAllDiffs = async () => { - const filesToFetch = changedFiles.filter((file) => !allDiffs.has(file.path)); - if (filesToFetch.length === 0) return; - - // Mark all as loading - setLoadingFiles((prev) => { - const next = new Set(prev); - filesToFetch.forEach((f) => next.add(f.path)); - return next; - }); - - // Fetch all in parallel - const results = await Promise.allSettled( - filesToFetch.map(async (file) => { - const dirState = useGitStore.getState().directories.get(effectiveDirectory); - const cached = dirState?.diffCache.get(file.path); - if (cached && cached.fetchedAt >= (dirState?.lastStatusChange || 0)) { - return { path: file.path, diff: { original: cached.original, modified: cached.modified } }; - } - - const response = await git.getGitFileDiff(effectiveDirectory, { path: file.path }); - const diff = { original: response.original ?? '', modified: response.modified ?? '' }; - setDiff(effectiveDirectory, file.path, diff); - return { path: file.path, diff }; - }) - ); - - // Update state with all fetched diffs - setAllDiffs((prev) => { - const next = new Map(prev); - results.forEach((result) => { - if (result.status === 'fulfilled') { - next.set(result.value.path, result.value.diff); - } - }); - return next; - }); - - // Clear loading state - setLoadingFiles((prev) => { - const next = new Set(prev); - filesToFetch.forEach((f) => next.delete(f.path)); - return next; - }); - }; - - fetchAllDiffs(); - }, [effectiveDirectory, changedFiles, git, setDiff]); // eslint-disable-line react-hooks/exhaustive-deps - - // Clear all diffs when directory changes - React.useEffect(() => { - setAllDiffs(new Map()); - setLoadingFiles(new Set()); - }, [effectiveDirectory]); - - // Re-fetch stale diffs when status changes (don't clear - keep old ones visible while fetching) - React.useEffect(() => { - if (!effectiveDirectory || !lastStatusChange || changedFiles.length === 0) return; - - const refetchStaleDiffs = async () => { - const dirState = useGitStore.getState().directories.get(effectiveDirectory); - if (!dirState) return; - - // Find files that need refetching (stale cache or no cache) - const staleFiles = changedFiles.filter((file) => { - const cached = dirState.diffCache.get(file.path); - return !cached || cached.fetchedAt < lastStatusChange; - }); - - if (staleFiles.length === 0) return; - - // Mark as loading - setLoadingFiles((prev) => { - const next = new Set(prev); - staleFiles.forEach((f) => next.add(f.path)); - return next; - }); - - // Fetch in parallel - const results = await Promise.allSettled( - staleFiles.map(async (file) => { - const response = await git.getGitFileDiff(effectiveDirectory, { path: file.path }); - const diff = { original: response.original ?? '', modified: response.modified ?? '' }; - setDiff(effectiveDirectory, file.path, diff); - return { path: file.path, diff }; - }) - ); - - // Update diffs in place (don't clear old ones first) - setAllDiffs((prev) => { - const next = new Map(prev); - results.forEach((result) => { - if (result.status === 'fulfilled') { - next.set(result.value.path, result.value.diff); - } - }); - // Remove diffs for files that no longer exist in changedFiles - for (const [filePath] of prev) { - if (!changedFiles.some((f) => f.path === filePath)) { - next.delete(filePath); - } - } - return next; - }); - - // Clear loading state - setLoadingFiles((prev) => { - const next = new Set(prev); - staleFiles.forEach((f) => next.delete(f.path)); - return next; - }); - }; - - refetchStaleDiffs(); - }, [effectiveDirectory, lastStatusChange, changedFiles, git, setDiff]); - const handleSelectFile = React.useCallback((value: string) => { setSelectedFile(value); }, []); const renderSideBySide = (currentLayoutForSelectedFile ?? 'side-by-side') === 'side-by-side'; + const selectedCachedDiff = useGitStore(React.useCallback((state) => { + if (!effectiveDirectory || !selectedFile) return null; + return state.directories.get(effectiveDirectory)?.diffCache.get(selectedFile) ?? null; + }, [effectiveDirectory, selectedFile])); + + const hasCurrentDiff = !!selectedCachedDiff; + const isCurrentFileLoading = !!selectedFile && !hasCurrentDiff; + // Render all diff viewers - they stay mounted const renderAllDiffViewers = () => { - if (allDiffs.size === 0) return null; + if (!effectiveDirectory || changedFiles.length === 0) return null; - return Array.from(allDiffs.entries()).map(([filePath, diff]) => ( - ( + @@ -528,8 +449,6 @@ export const DiffView: React.FC = () => { ); } - const isCurrentFileLoading = selectedFile && loadingFiles.has(selectedFile); - const hasCurrentDiff = selectedFile && allDiffs.has(selectedFile); return (
diff --git a/packages/ui/src/contexts/DiffWorkerProvider.tsx b/packages/ui/src/contexts/DiffWorkerProvider.tsx index e67da7d7..c6cc8f31 100644 --- a/packages/ui/src/contexts/DiffWorkerProvider.tsx +++ b/packages/ui/src/contexts/DiffWorkerProvider.tsx @@ -1,24 +1,176 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect, useRef } from 'react'; import { WorkerPoolContextProvider, useWorkerPool } from '@pierre/diffs/react'; +import { parseDiffFromFile, type FileContents } from '@pierre/diffs'; import type { SupportedLanguages } from '@pierre/diffs'; import { useOptionalThemeSystem } from './useThemeSystem'; import { workerFactory } from '@/lib/diff/workerFactory'; import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes'; import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes'; +import { useGitStore } from '@/stores/useGitStore'; +import { getLanguageFromExtension } from '@/lib/toolHelpers'; -// Only preload the most common languages - others load on demand +// Preload common languages for faster initial diff rendering const PRELOAD_LANGS: SupportedLanguages[] = [ 'typescript', 'javascript', 'tsx', + 'jsx', 'json', + 'html', + 'css', + 'markdown', + 'yaml', + 'python', + 'rust', + 'go', + 'bash', ]; +// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx` +function getPierreCacheKey(fileName: string, original: string, modified: string): string { + const sampleOriginal = original.length > 100 + ? `${original.slice(0, 50)}${original.slice(-50)}` + : original; + const sampleModified = modified.length > 100 + ? `${modified.slice(0, 50)}${modified.slice(-50)}` + : modified; + return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`; +} + interface DiffWorkerProviderProps { children: React.ReactNode; } +type IdleDeadlineLike = { timeRemaining(): number; didTimeout: boolean }; + +function scheduleWarmupWork(cb: (deadline?: IdleDeadlineLike) => void): () => void { + if (typeof window === 'undefined') { + return () => {}; + } + + const id = window.setTimeout(() => cb(undefined), 0); + return () => window.clearTimeout(id); +} + +// Component that warms up the worker pool and precomputes diff ASTs +const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const workerPool = useWorkerPool(); + const activeDirectory = useGitStore((state) => state.activeDirectory); + const lastStatusChange = useGitStore((state) => { + if (!activeDirectory) return 0; + return state.directories.get(activeDirectory)?.lastStatusChange ?? 0; + }); + const diffCacheSize = useGitStore((state) => { + if (!activeDirectory) return 0; + return state.directories.get(activeDirectory)?.diffCache.size ?? 0; + }); + + const didDummyWarmupRef = useRef(false); + const warmedStatusRef = useRef(new Map()); + + useEffect(() => { + if (!workerPool || didDummyWarmupRef.current) return; + + didDummyWarmupRef.current = true; + + const dummyFile: FileContents = { + name: 'warmup.ts', + contents: 'const x = 1;', + lang: 'typescript', + cacheKey: 'warmup-file', + }; + + const dummyDiff = parseDiffFromFile(dummyFile, { ...dummyFile, contents: 'const x = 2;', cacheKey: 'warmup-file-2' }); + + const dummyInstance = { + onHighlightSuccess: () => {}, + onHighlightError: () => {}, + }; + + workerPool.highlightDiffAST(dummyInstance, dummyDiff); + }, [workerPool]); + + useEffect(() => { + if (!workerPool || !activeDirectory) return; + if (!lastStatusChange || diffCacheSize === 0) return; + + const dirState = useGitStore.getState().directories.get(activeDirectory); + if (!dirState || dirState.diffCache.size === 0) return; + + const alreadyWarmedAt = warmedStatusRef.current.get(activeDirectory) ?? 0; + if (alreadyWarmedAt >= dirState.lastStatusChange) return; + + // Only warm once per status-change tick + warmedStatusRef.current.set(activeDirectory, dirState.lastStatusChange); + + let cancelled = false; + let cancelScheduled: (() => void) | null = null; + + const entries = Array.from(dirState.diffCache.entries()); + + let index = 0; + + const processChunk = (deadline?: IdleDeadlineLike) => { + if (cancelled) return; + + const chunkStart = Date.now(); + while (index < entries.length) { + if (deadline && deadline.timeRemaining() < 8) break; + if (!deadline && Date.now() - chunkStart > 8) break; + + const [filePath, diff] = entries[index]; + index += 1; + + const language = getLanguageFromExtension(filePath) || 'text'; + const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified); + + const oldFile: FileContents = { + name: filePath, + contents: diff.original, + lang: language as FileContents['lang'], + cacheKey: `old-${cacheKey}`, + }; + + const newFile: FileContents = { + name: filePath, + contents: diff.modified, + lang: language as FileContents['lang'], + cacheKey: `new-${cacheKey}`, + }; + + const fileDiff = parseDiffFromFile(oldFile, newFile); + + // Use a unique instance per request so Pierre doesn't ignore earlier results. + const instance = { + onHighlightSuccess: () => {}, + onHighlightError: () => {}, + }; + + workerPool.highlightDiffAST(instance, fileDiff); + } + + if (index < entries.length) { + cancelScheduled = scheduleWarmupWork(processChunk); + } + }; + + // Kick off immediately, then continue in chunks. + processChunk(undefined); + + if (index < entries.length) { + cancelScheduled = scheduleWarmupWork(processChunk); + } + + return () => { + cancelled = true; + cancelScheduled?.(); + }; + }, [workerPool, activeDirectory, lastStatusChange, diffCacheSize]); + + return <>{children}; +}; + export const DiffWorkerProvider: React.FC = ({ children }) => { const themeSystem = useOptionalThemeSystem(); const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark'; @@ -43,7 +195,9 @@ export const DiffWorkerProvider: React.FC = ({ children }} highlighterOptions={highlighterOptions} > - {children} + + {children} + ); }; diff --git a/packages/ui/src/stores/useGitStore.ts b/packages/ui/src/stores/useGitStore.ts index 7bd65224..70a264dc 100644 --- a/packages/ui/src/stores/useGitStore.ts +++ b/packages/ui/src/stores/useGitStore.ts @@ -7,10 +7,10 @@ import type { GitIdentitySummary, } from '@/lib/api/types'; -const GIT_POLL_BASE_INTERVAL = 10000; -const GIT_POLL_MAX_INTERVAL = 20000; +const GIT_POLL_BASE_INTERVAL = 5000; +const GIT_POLL_MAX_INTERVAL = 10000; const GIT_POLL_BACKOFF_STEP = 5000; -const LOG_STALE_THRESHOLD = 30000; +const LOG_STALE_THRESHOLD = 10000; interface DirectoryGitState { isGitRepo: boolean | null; @@ -51,6 +51,7 @@ interface GitStore { getDiff: (directory: string, filePath: string) => { original: string; modified: string; fetchedAt: number } | null; setDiff: (directory: string, filePath: string, diff: { original: string; modified: string }) => void; clearDiffCache: (directory: string) => void; + fetchAllDiffs: (directory: string, git: GitAPI) => Promise; setLogMaxCount: (directory: string, maxCount: number) => void; @@ -60,12 +61,19 @@ interface GitStore { refresh: (git: GitAPI, options?: { force?: boolean }) => Promise; } +interface GitFileDiffResponse { + original: string; + modified: string; + path: string; +} + interface GitAPI { checkIsGitRepository: (directory: string) => Promise; getGitStatus: (directory: string) => Promise; getGitBranches: (directory: string) => Promise; getGitLog: (directory: string, options?: { maxCount?: number }) => Promise; getCurrentGitIdentity: (directory: string) => Promise; + getGitFileDiff: (directory: string, options: { path: string }) => Promise; } const createEmptyDirectoryState = (): DirectoryGitState => ({ @@ -132,6 +140,55 @@ const hasStatusChanged = (oldStatus: GitStatus | null, newStatus: GitStatus | nu return false; }; +const getChangedFilePaths = (oldStatus: GitStatus | null, newStatus: GitStatus | null): Set => { + const changed = new Set(); + if (!newStatus) return changed; + + const oldFiles = oldStatus?.files ?? []; + const newFiles = newStatus.files ?? []; + + const oldFileMap = new Map(oldFiles.map((f) => [f.path, f] as const)); + const newFileMap = new Map(newFiles.map((f) => [f.path, f] as const)); + + const allFilePaths = new Set([...oldFileMap.keys(), ...newFileMap.keys()]); + for (const filePath of allFilePaths) { + const oldFile = oldFileMap.get(filePath); + const newFile = newFileMap.get(filePath); + + // Added/removed/renamed + if (!oldFile || !newFile) { + changed.add(filePath); + continue; + } + + // Index/worktree state changed (indicates actual content/state changed) + if (oldFile.index !== newFile.index || oldFile.working_dir !== newFile.working_dir) { + changed.add(filePath); + continue; + } + } + + const oldStats = oldStatus?.diffStats ?? {}; + const newStats = newStatus.diffStats ?? {}; + const allStatPaths = new Set([...Object.keys(oldStats), ...Object.keys(newStats)]); + + for (const filePath of allStatPaths) { + const oldEntry = oldStats[filePath]; + const newEntry = newStats[filePath]; + + if (!oldEntry || !newEntry) { + changed.add(filePath); + continue; + } + + if (oldEntry.insertions !== newEntry.insertions || oldEntry.deletions !== newEntry.deletions) { + changed.add(filePath); + } + } + + return changed; +}; + export const useGitStore = create()( devtools( (set, get) => ({ @@ -198,13 +255,34 @@ export const useGitStore = create()( const newDirectories = new Map(get().directories); const currentDirState = newDirectories.get(directory) ?? createEmptyDirectoryState(); + const changedPaths = getChangedFilePaths(currentDirState.status, newStatus); + + const oldPaths = new Set((currentDirState.status?.files ?? []).map((f) => f.path)); + const newPaths = new Set((newStatus.files ?? []).map((f) => f.path)); + + const nextDiffCache = new Map(currentDirState.diffCache); + + // Drop cache for removed files + for (const oldPath of oldPaths) { + if (!newPaths.has(oldPath)) { + nextDiffCache.delete(oldPath); + } + } + + // Drop cache for files whose state/content changed + for (const filePath of changedPaths) { + nextDiffCache.delete(filePath); + } + + const hasFileContentChange = changedPaths.size > 0; + newDirectories.set(directory, { ...currentDirState, isGitRepo: true, status: newStatus, - diffCache: new Map(), + diffCache: nextDiffCache, lastStatusFetch: Date.now(), - lastStatusChange: Date.now(), + lastStatusChange: hasFileContentChange ? Date.now() : currentDirState.lastStatusChange, }); set({ directories: newDirectories }); } else { @@ -314,6 +392,9 @@ export const useGitStore = create()( } await get().fetchIdentity(directory, git); + + // Pre-fetch all diffs so they're ready when user opens Diff tab + await get().fetchAllDiffs(directory, git); }, getDiff: (directory, filePath) => { @@ -339,6 +420,49 @@ export const useGitStore = create()( } }, + fetchAllDiffs: async (directory, git) => { + const dirState = get().directories.get(directory); + if (!dirState?.status?.files || dirState.status.files.length === 0) return; + + const files = dirState.status.files; + + // Find files that need fetching (no cache) + const filesToFetch = files.filter((file) => !dirState.diffCache.has(file.path)); + + if (filesToFetch.length === 0) return; + + // Fetch all diffs in parallel + const results = await Promise.allSettled( + filesToFetch.map(async (file) => { + const response = await git.getGitFileDiff(directory, { path: file.path }); + return { + path: file.path, + diff: { original: response.original ?? '', modified: response.modified ?? '' } + }; + }) + ); + + // Update diff cache with results + const newDirectories = new Map(get().directories); + const currentDirState = newDirectories.get(directory); + if (!currentDirState) return; + + const newDiffCache = new Map(currentDirState.diffCache); + const now = Date.now(); + + results.forEach((result) => { + if (result.status === 'fulfilled') { + newDiffCache.set(result.value.path, { + ...result.value.diff, + fetchedAt: now + }); + } + }); + + newDirectories.set(directory, { ...currentDirState, diffCache: newDiffCache }); + set({ directories: newDirectories }); + }, + setLogMaxCount: (directory, maxCount) => { const newDirectories = new Map(get().directories); const dirState = newDirectories.get(directory) ?? createEmptyDirectoryState(); @@ -368,6 +492,8 @@ export const useGitStore = create()( const statusChanged = await get().fetchStatus(activeDirectory, git, { silent: true }); if (statusChanged) { await get().fetchLog(activeDirectory, git); + // Pre-fetch all diffs so they're ready when user opens Diff tab + await get().fetchAllDiffs(activeDirectory, git); // Reset to base interval on changes set({ currentPollInterval: GIT_POLL_BASE_INTERVAL }); } else { diff --git a/packages/web/public/favicon.png b/packages/web/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fdd93134ae600e3ebfe32d526ffa0f2e24c3e32 GIT binary patch literal 2158 zcmcIlX;hO}8vaK1B`|Za1(cAPgej)Gz5Ta833>=gRU(BKpY4F zE5QJOV*-HoMOKrCt?c3mg-mb+fa;qFjO?UDC%QT*Evx@cL*8_6@x}!igrqz9(!)aN z;kZz0xGVq|5MybI#+ah5&!H`Fpasst!WaZ`AgIjy{>T3!M8|}kkGT9{!ey3WvW#H# z#|!lH(cv_DNc6=Ib57n+V$16EBs!ixmoT!d- z8X#<353}D~4{C4h^@3f~*G~6vmgMh@noTbDnX~cD$hNa7%1K6Yn15X;H1YL}erJS5 znlI?NGy2ilPHuuAPguKtPq(15ah~DhAAQv23jsPZxue#m3>b@*7Y`3)Jt`t$y!|)z zp3Pe&RGH)M%`s~wT32UsP|&!Q(-0b^g`aG~Hi+aYT0^`gugT#b`-k9Pyp0|V= z!ea}eBF+W5mKi3M-_>1AZO66=kE;e7e8qh8K&a#eMv@vg37WcCEe+;QW6rEy{#|}D ziA-qNObL-*{c>!8ufyOalU${KMa}He(G6G0_Za43t;ilPX}R6CIKke|;~BjOK1u;q z(K`Ru$vLC`1zeH+*L08T|08@Sx@)U#t)KfTVnnnomqd-vQsHt+xm<2p8J9CIL5jM& zQN>_sd3m``pR_)tL(cfb*cG3euuK(h2+yuzB9;%?sfik0yWb*5zkb$%@*BnKfHQ~ zAjh$j*KMZs`8I~^ovz!T**B|y(wk_5#VT}YRFrAnqDWUoh|%1@3*5SHHkOpKC8JhWRpMol7t^Dco*)Bd-*n<*t#_64Uxrpk|8e-w<&w!h#~iv)@3UMq5}D573|HA zr$2dOari^-$fWVxHS-5|TT406(L^p=Ch=0la!N!czIWUmLnV=Lm~3b+yOdqNx{elp zc@QuC7NDy3(EHaxUYHc*_S(#56lSkZ%GagHB&g+PL+LDFP_kYLM zost(um{Bp|fqt8Lf(MOgS6O6<3$!8%5USkZPOXqH?)V4;r-iCTYJJh)3lG-jIDJwM%tCd?>RkaIWi$4{kJ)xl0bMWe9#f!BP zY7a|%5a9Xq{65H=n{(M-NfG!OvVTa!)(q_UKNaPH5+_Phf%B#7#-Q( zwvgFYZ+x6ogUv=429!L7iyEvalg}(iRaI5m0M)Hyrz0H{#VFBDf9v5l?)9^&tfAme zv%?&uD6ws(1i3Tu^Xu)TpwFrEI*RCLasPm*c@G?~JvG!gu~DL1zbP*cSaBF}&_!a^ zPClD0Ok{U0Mft_@Khd@YIpZ6#+b)7>FIaov34w2H2koTaH}7>IJN}zcV>i^^WDl&B z(Z=~a!|ut{_Va~8a$ucu(9+hc;K2zRvCq<6Rj2EYsAkV<3%**qHNIR}f>bL!cPJ5^ zmE60}N<%5RpgP6%wtm)Sw;nXiwtlOpzOX2*Aaq_Lj$1tzi+to{hW#f44k3<-4y~*c TRxfa56B!^nxj8;O6P)%NE3dcP literal 0 HcmV?d00001 diff --git a/packages/web/public/vite.svg b/packages/web/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/packages/web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file