From bb471a0917477e26bd373e6695582369016c7a4a Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Wed, 24 Jun 2026 07:08:43 -0400 Subject: [PATCH] Improve workspace/chat diff viewer with syntax highlighting & per-file view Replace the raw single-blob diff dump (Monaco, language=diff) and the plain
 file diffs in chat with @git-diff-view/react:
- Parse the unified git diff into structured per-file entries (status,
  +/- counts, binary detection) via parseDiffFiles()
- Workspace Diff tab: collapsible per-file cards with status badges, line
  counts, syntax highlighting, and a Unified/Split toggle
- Agent chat: render each change's diff highlighted instead of plain text
- Theme follows next-themes resolvedTheme (light/dark)
---
 apps/next/package.json                        |   2 +
 .../agent-workspace/agent-thread.tsx          | 126 +++++++-----
 .../agent-workspace/diff-file-view.tsx        |  68 +++++++
 .../components/agent-workspace/diff-utils.ts  | 109 +++++++++++
 .../agent-workspace/diff-viewer.tsx           | 181 ++++++++++++++----
 bun.lock                                      |  25 +++
 6 files changed, 420 insertions(+), 91 deletions(-)
 create mode 100644 apps/next/src/components/agent-workspace/diff-file-view.tsx

diff --git a/apps/next/package.json b/apps/next/package.json
index 442f1d0..726f727 100644
--- a/apps/next/package.json
+++ b/apps/next/package.json
@@ -21,6 +21,7 @@
   },
   "dependencies": {
     "@convex-dev/auth": "catalog:convex",
+    "@git-diff-view/react": "^0.1.6",
     "@monaco-editor/react": "latest",
     "@sentry/nextjs": "^10.46.0",
     "@spoon/backend": "workspace:*",
@@ -31,6 +32,7 @@
     "monaco-vim": "latest",
     "next": "^16.2.1",
     "next-plausible": "^3.12.5",
+    "next-themes": "^0.4.6",
     "react": "catalog:react19",
     "react-dom": "catalog:react19",
     "require-in-the-middle": "^7.5.2",
diff --git a/apps/next/src/components/agent-workspace/agent-thread.tsx b/apps/next/src/components/agent-workspace/agent-thread.tsx
index 55c9c8c..9a7bff5 100644
--- a/apps/next/src/components/agent-workspace/agent-thread.tsx
+++ b/apps/next/src/components/agent-workspace/agent-thread.tsx
@@ -14,7 +14,8 @@ import { toast } from 'sonner';
 import type { Doc } from '@spoon/backend/convex/_generated/dataModel.js';
 import { Badge, Button, Textarea } from '@spoon/ui';
 
-import { extractFileDiff } from './diff-utils';
+import { DiffFileView, useDiffTheme } from './diff-file-view';
+import { parseDiffFileForPath } from './diff-utils';
 
 type ActivityFilter = 'all' | 'chat' | 'activity' | 'files' | 'errors';
 
@@ -67,6 +68,7 @@ export const AgentThread = ({
   const [sending, setSending] = useState(false);
   const [replying, setReplying] = useState();
   const [filter, setFilter] = useState('all');
+  const diffTheme = useDiffTheme();
   const scrollRef = useRef(null);
   const chatMessages = useMemo(
     () =>
@@ -317,58 +319,80 @@ export const AgentThread = ({
             
))} - {visibleChanges.map((change) => ( -
-
-
-
- - - {change.path} - + {visibleChanges.map((change) => { + const changedFile = parseDiffFileForPath(change.diff, change.path); + const hasDiff = Boolean(changedFile && !changedFile.isBinary); + const hasRenderableHunk = Boolean( + changedFile && hasDiff && changedFile.hunkText.includes('@@'), + ); + return ( +
+
+
+
+ + + {change.path} + +
+

+ {change.source} {change.changeType} + {changedFile ? ( + + + +{changedFile.additions} + {' '} + + −{changedFile.deletions} + + + ) : null} +

+
+
+ {hasDiff ? ( + + ) : null} + {change.path !== '.' ? ( + + ) : null}
-

- {change.source} {change.changeType} -

-
- {extractFileDiff(change.diff, change.path) ? ( - - ) : null} - {change.path !== '.' ? ( - - ) : null} -
-
- {extractFileDiff(change.diff, change.path) ? ( -
- - File diff - -
-                  {extractFileDiff(change.diff, change.path)}
-                
-
- ) : null} -
- ))} + {hasRenderableHunk && changedFile ? ( +
+ + File diff + +
+ +
+
+ ) : null} + + ); + })} {visibleEvents.slice(-80).map((event) => (
{ + const { resolvedTheme } = useTheme(); + return resolvedTheme === 'light' ? 'light' : 'dark'; +}; + +/** + * Renders one file's diff with syntax highlighting. Falls back to a short note + * for binary files and metadata-only changes (pure renames / mode changes) that + * have no hunks to display. + */ +export const DiffFileView = ({ + file, + mode, + theme, + fontSize = 12, + wrap = false, +}: { + file: ParsedDiffFile; + mode: DiffMode; + theme: 'light' | 'dark'; + fontSize?: number; + wrap?: boolean; +}) => { + if (file.isBinary) { + return ( +
+ Binary file not shown. +
+ ); + } + if (!file.hunkText.includes('@@')) { + return ( +
+ {file.status === 'renamed' + ? 'Renamed with no content changes.' + : 'No content changes.'} +
+ ); + } + return ( + + ); +}; diff --git a/apps/next/src/components/agent-workspace/diff-utils.ts b/apps/next/src/components/agent-workspace/diff-utils.ts index 0eba7f0..6e30a15 100644 --- a/apps/next/src/components/agent-workspace/diff-utils.ts +++ b/apps/next/src/components/agent-workspace/diff-utils.ts @@ -1,3 +1,112 @@ +export type DiffFileStatus = 'added' | 'deleted' | 'modified' | 'renamed'; + +export type ParsedDiffFile = { + id: string; + oldPath: string; + newPath: string; + /** Path to show in the UI (new path, or old path for deletions). */ + displayPath: string; + status: DiffFileStatus; + additions: number; + deletions: number; + isBinary: boolean; + /** The full per-file unified diff section, fed as-is to the diff renderer. */ + hunkText: string; +}; + +const stripABPrefix = (value: string) => value.replace(/^[ab]\//, ''); + +/** + * Splits a raw unified git diff into structured, per-file entries. Replaces the + * "one giant blob" rendering: each file can be shown, counted, and highlighted + * independently. + */ +export const parseDiffFiles = (diff: string | undefined): ParsedDiffFile[] => { + if (!diff?.trim()) return []; + const sections: string[][] = []; + let current: string[] | null = null; + for (const line of diff.split('\n')) { + if (line.startsWith('diff --git ')) { + if (current) sections.push(current); + current = [line]; + continue; + } + current?.push(line); + } + if (current) sections.push(current); + + return sections.map((sectionLines, index) => { + const header = sectionLines[0] ?? ''; + const gitMatch = /^diff --git a\/(.+?) b\/(.+)$/.exec(header); + let oldPath = gitMatch?.[1] ?? ''; + let newPath = gitMatch?.[2] ?? oldPath; + let status: DiffFileStatus = 'modified'; + let isBinary = false; + let additions = 0; + let deletions = 0; + let renameFrom = ''; + let renameTo = ''; + + for (const line of sectionLines) { + if (line.startsWith('new file mode')) status = 'added'; + else if (line.startsWith('deleted file mode')) status = 'deleted'; + else if (line.startsWith('rename from ')) { + renameFrom = line.slice('rename from '.length); + } else if (line.startsWith('rename to ')) { + renameTo = line.slice('rename to '.length); + } else if ( + line.startsWith('Binary files') || + line.startsWith('GIT binary patch') + ) { + isBinary = true; + } else if (line.startsWith('--- ')) { + const value = line.slice(4).trim(); + if (value !== '/dev/null') oldPath = stripABPrefix(value); + } else if (line.startsWith('+++ ')) { + const value = line.slice(4).trim(); + if (value !== '/dev/null') newPath = stripABPrefix(value); + } else if (line.startsWith('+') && !line.startsWith('+++')) { + additions += 1; + } else if (line.startsWith('-') && !line.startsWith('---')) { + deletions += 1; + } + } + + if (renameFrom || renameTo) { + status = 'renamed'; + oldPath = renameFrom || oldPath; + newPath = renameTo || newPath; + } + + const displayPath = status === 'deleted' ? oldPath : newPath; + return { + id: `${index}-${displayPath}`, + oldPath, + newPath, + displayPath, + status, + additions, + deletions, + isBinary, + hunkText: sectionLines.join('\n'), + }; + }); +}; + +/** Returns the single parsed file matching a path, if present in the diff. */ +export const parseDiffFileForPath = ( + diff: string | undefined, + filePath: string, +): ParsedDiffFile | undefined => { + const normalized = filePath.replace(/^\.\/+/, ''); + return parseDiffFiles(diff).find( + (file) => + file.displayPath === normalized || + file.newPath === normalized || + file.oldPath === normalized, + ); +}; + export const extractFileDiff = (diff: string | undefined, filePath: string) => { if (!diff?.trim() || filePath === '.') return ''; const lines = diff.split('\n'); diff --git a/apps/next/src/components/agent-workspace/diff-viewer.tsx b/apps/next/src/components/agent-workspace/diff-viewer.tsx index 3ad7aaf..e75ba1c 100644 --- a/apps/next/src/components/agent-workspace/diff-viewer.tsx +++ b/apps/next/src/components/agent-workspace/diff-viewer.tsx @@ -1,25 +1,90 @@ 'use client'; -import dynamic from 'next/dynamic'; +import { useMemo, useState } from 'react'; +import { ChevronDown, ChevronRight } from 'lucide-react'; import { Button } from '@spoon/ui'; -import { extractFileDiff } from './diff-utils'; +import type { DiffMode } from './diff-file-view'; +import type { DiffFileStatus, ParsedDiffFile } from './diff-utils'; +import { DiffFileView, useDiffTheme } from './diff-file-view'; +import { parseDiffFiles } from './diff-utils'; -const MonacoEditor = dynamic(async () => await import('@monaco-editor/react'), { - ssr: false, -}); +const statusBadge: Record< + DiffFileStatus, + { label: string; className: string } +> = { + added: { label: 'Added', className: 'bg-emerald-500/15 text-emerald-500' }, + deleted: { label: 'Deleted', className: 'bg-red-500/15 text-red-500' }, + modified: { label: 'Modified', className: 'bg-amber-500/15 text-amber-500' }, + renamed: { label: 'Renamed', className: 'bg-sky-500/15 text-sky-500' }, +}; -const diffStats = (diff: string) => { - const files = new Set(); - let additions = 0; - let removals = 0; - for (const line of diff.split('\n')) { - if (line.startsWith('diff --git ')) files.add(line); - if (line.startsWith('+') && !line.startsWith('+++')) additions += 1; - if (line.startsWith('-') && !line.startsWith('---')) removals += 1; - } - return { files: files.size, additions, removals }; +const totals = (files: ParsedDiffFile[]) => + files.reduce( + (acc, file) => ({ + additions: acc.additions + file.additions, + deletions: acc.deletions + file.deletions, + }), + { additions: 0, deletions: 0 }, + ); + +const FileCard = ({ + file, + mode, + theme, + defaultOpen, +}: { + file: ParsedDiffFile; + mode: DiffMode; + theme: 'light' | 'dark'; + defaultOpen: boolean; +}) => { + const [open, setOpen] = useState(defaultOpen); + const badge = statusBadge[file.status]; + return ( +
+ + {open ? ( +
+ +
+ ) : null} +
+ ); }; export const DiffViewer = ({ @@ -33,25 +98,65 @@ export const DiffViewer = ({ onRefresh: () => Promise; onClearFocusedPath?: () => void; }) => { - const focusedDiff = focusedPath ? extractFileDiff(diff, focusedPath) : ''; - const visibleDiff = focusedPath ? focusedDiff : diff; - const stats = diffStats(visibleDiff); + const [mode, setMode] = useState('unified'); + const theme = useDiffTheme(); + + const files = useMemo(() => parseDiffFiles(diff), [diff]); + const normalizedFocus = focusedPath?.replace(/^\.\/+/, ''); + const visibleFiles = useMemo( + () => + normalizedFocus + ? files.filter( + (file) => + file.displayPath === normalizedFocus || + file.newPath === normalizedFocus || + file.oldPath === normalizedFocus, + ) + : files, + [files, normalizedFocus], + ); + const stats = totals(visibleFiles); + return (

- {focusedPath ? `Diff viewer: ${focusedPath}` : 'Diff viewer'} + {focusedPath ? `Diff: ${focusedPath}` : 'Diff viewer'}

- {visibleDiff.trim() - ? `${stats.files} files, +${stats.additions} -${stats.removals}` - : focusedPath - ? 'No diff for this file' - : 'Current git diff'} + {visibleFiles.length > 0 + ? `${visibleFiles.length} ${visibleFiles.length === 1 ? 'file' : 'files'}, ` + : ''} + +{stats.additions}{' '} + −{stats.deletions}

+
+ + +
{focusedPath ? (
- {visibleDiff.trim() ? ( - + {visibleFiles.length > 0 ? ( +
+ {visibleFiles.map((file, index) => ( + + ))} +
) : (
{focusedPath diff --git a/bun.lock b/bun.lock index 5a272b8..49c0e97 100644 --- a/bun.lock +++ b/bun.lock @@ -97,6 +97,7 @@ "version": "0.1.0", "dependencies": { "@convex-dev/auth": "catalog:convex", + "@git-diff-view/react": "^0.1.6", "@monaco-editor/react": "latest", "@sentry/nextjs": "^10.46.0", "@spoon/backend": "workspace:*", @@ -705,6 +706,12 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@git-diff-view/core": ["@git-diff-view/core@0.1.6", "", { "dependencies": { "@git-diff-view/lowlight": "^0.1.6", "fast-diff": "^1.3.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0" } }, "sha512-q2Ch8jURF6pL7VeNpOgHBRVY9gsGLXCOYpKXHG3BqpXe0kv6GNSUux8SmAYsDrakBzfgDClODxDtsM2rfiWpnA=="], + + "@git-diff-view/lowlight": ["@git-diff-view/lowlight@0.1.6", "", { "dependencies": { "@types/hast": "^3.0.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0" } }, "sha512-YIsiAc2aWAePWaDNi3k8xI0Vs/ZItt5J6nrftTIFbMFN3GwDOsyJFm2L7o8XWKTJkV2yItaz28KUI9CWj0MVZA=="], + + "@git-diff-view/react": ["@git-diff-view/react@0.1.6", "", { "dependencies": { "@git-diff-view/core": "^0.1.6", "@types/hast": "^3.0.0", "fast-diff": "^1.3.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0", "reactivity-store": "^0.4.0", "use-sync-external-store": "^1.6.0" }, "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-koABBon5bNKh6/WnWSxggK9ojw+cvWAPnY2/ciOkwlR+8dm0h6A7Qa5kP2HFDxqYHwZ2imkGMcSLgXMOnWHRFA=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -1525,6 +1532,8 @@ "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], @@ -1553,6 +1562,8 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -1605,6 +1616,10 @@ "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "@vue/reactivity": ["@vue/reactivity@3.5.38", "", { "dependencies": { "@vue/shared": "3.5.38" } }, "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ=="], + + "@vue/shared": ["@vue/shared@3.5.38", "", {}, "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug=="], + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], @@ -1965,6 +1980,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], @@ -2157,6 +2174,8 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -2277,6 +2296,8 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -2535,6 +2556,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="], @@ -2877,6 +2900,8 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "reactivity-store": ["reactivity-store@0.4.0", "", { "dependencies": { "@vue/reactivity": "~3.5.30", "@vue/shared": "~3.5.30", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-uL9uoREOBg2o4zUa8vMU0AbvAOk0osPloizscmyZqMvJzcuuKX3ELFYYr1DX8gAcfvlhPduz4QuLZn1eChCu4Q=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],