Skip to content

[wip] Commit diffs#1154

Open
brendan-kellam wants to merge 10 commits intomainfrom
bkellam/commit-diffs
Open

[wip] Commit diffs#1154
brendan-kellam wants to merge 10 commits intomainfrom
bkellam/commit-diffs

Conversation

@brendan-kellam
Copy link
Copy Markdown
Contributor

@brendan-kellam brendan-kellam commented Apr 28, 2026

Summary by CodeRabbit

  • New Features

    • Added a full commit-diff experience: commit header, authors, file list, per-file diffs, and an inline lightweight diff viewer with syntax highlighting and copy actions.
    • Commit routes and history rows now navigate to commit-diff views.
  • Improvements

    • New unified code-highlighting backend with CodeMirror parsing; SHA truncation and breadcrumb sizing improved.
    • Commit messages are expandable; diff statistics and visual change indicators added.
  • Chores

    • Added CodeMirror merge dependency and conditional dev script loading controlled by a new env flag.
    • Reorganized imports and logging utilities.

@github-actions
Copy link
Copy Markdown
Contributor

@brendan-kellam your pull request is missing a changelog!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d312fa33-3440-4943-91e0-3eec894115e8

📥 Commits

Reviewing files that changed from the base of the PR and between bb56039 and 5c36e73.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (36)
  • packages/shared/src/env.server.ts
  • packages/web/package.json
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/pureCodePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/rangeHighlightingExtension.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitDiffPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitHashLine.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitMessage.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/diffStat.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/hunkParser.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/lightweightDiffViewer.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/splitPairing.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/authorFilter.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitRow.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitsPagination.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitsPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/dateFilter.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/page.tsx
  • packages/web/src/app/(app)/browse/components/historyRow.tsx
  • packages/web/src/app/(app)/browse/hooks/useBrowseNavigation.ts
  • packages/web/src/app/(app)/browse/hooks/utils.ts
  • packages/web/src/app/(app)/components/lightweightCodeHighlighter.tsx
  • packages/web/src/app/(app)/components/pathHeader.tsx
  • packages/web/src/app/layout.tsx
  • packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx
  • packages/web/src/features/git/getFilesApi.ts
  • packages/web/src/features/git/getFolderContentsApi.ts
  • packages/web/src/features/git/getTreeApi.ts
  • packages/web/src/features/git/logger.ts
  • packages/web/src/features/git/utils.ts
  • packages/web/src/lib/codeHighlight.ts
  • packages/web/src/lib/utils.ts

Walkthrough

This PR introduces a commit diff viewing feature with dedicated components for displaying commit details and file-level diffs, adds shared code highlighting utilities, refactors imports to absolute paths throughout the browse module, extracts git logging, and adds a debug flag for React GRAP in development environments.

Changes

Cohort / File(s) Summary
Commit Diff Viewing Feature
commitDiffPanel/commitDiffPanel.tsx, commitDiffPanel/commitHashLine.tsx, commitDiffPanel/commitMessage.tsx, commitDiffPanel/fileDiffList.tsx, commitDiffPanel/fileDiffRow.tsx, commitDiffPanel/diffStat.tsx, commitDiffPanel/lightweightDiffViewer.tsx, commitDiffPanel/hunkParser.ts, commitDiffPanel/splitPairing.ts
Introduces comprehensive commit diff UI with async loading of commit details and diffs, parsing hunks into typed line structures, virtualizing large file lists, computing diff statistics, rendering two-column diff layouts with inner-diff highlighting and syntax coloring, and supporting renames and initial commits.
Browse Routing & Path Handling
browse/[...path]/page.tsx, browse/hooks/utils.ts, browse/hooks/useBrowseNavigation.ts
Adds commit path type to browse routing, updates getBrowseParamsFromPathParam and getBrowsePath to handle commit URLs (/-/commit/<commitSha>/<path?>), and refactors navigation hook to accept BrowseProps union type.
Import Path Refactoring
browse/[...path]/components/codePreviewPanel/pureCodePreviewPanel.tsx, browse/[...path]/components/codePreviewPanel/rangeHighlightingExtension.ts, browse/[...path]/components/commitHistoryPanel/commitRow.tsx, browse/[...path]/components/commitHistoryPanel/commitsPanel.tsx, browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx
Converts relative imports to absolute @/app/(app)/browse/... paths for browse utilities and components.
History Row Navigation
browse/components/historyRow.tsx, browse/components/commitHistoryPanel/commitRow.tsx
Adds router-based navigation to commit diff pages on row click, derives selected state from browse parameters, and updates styling to reflect clickability with cursor and hover feedback.
Shared Code Highlighting
lib/codeHighlight.ts, app/(app)/components/lightweightCodeHighlighter.tsx
Extracts Lezer-based syntax highlighting logic into new highlightCode utility supporting async parser loading by language/filename and range-based highlighting; delegates component to shared utility.
Git API Logger Extraction
features/git/logger.ts, features/git/utils.ts, features/git/getFilesApi.ts, features/git/getFolderContentsApi.ts, features/git/getTreeApi.ts
Creates dedicated logger module and updates APIs to import from ./logger instead of ./utils, removing logger export from utils.
Utility Enhancements & UI Updates
lib/utils.ts, app/(app)/components/pathHeader.tsx, features/chat/components/chatThread/tools/getDiffToolComponent.tsx
Adds truncateSha helper for shortening 40-char SHAs to 7 characters; updates pathHeader breadcrumb layout and visibility logic; refactors getDiffToolComponent to use shared truncateSha.
Environment & Dependencies
packages/shared/src/env.server.ts, packages/web/package.json, app/layout.tsx
Adds DEBUG_ENABLE_REACT_GRAP boolean config flag; adds @codemirror/merge dependency; gates react-grab script loading to development + flag enabled.

Sequence Diagram

sequenceDiagram
    participant User
    participant BrowsePage
    participant Router
    participant CommitDiffPanel
    participant DiffAPI
    participant FileDiffList
    participant LightweightDiffViewer
    participant CodeHighlighter

    User->>Router: Navigate to /-/commit/abc123/path
    Router->>BrowsePage: Match commit route with pathType='commit'
    BrowsePage->>CommitDiffPanel: Render with commitSha
    CommitDiffPanel->>DiffAPI: getCommit(commitSha)
    CommitDiffPanel->>DiffAPI: getDiff(parentSha..commitSha)
    DiffAPI-->>CommitDiffPanel: Return commit & diff
    CommitDiffPanel->>CommitDiffPanel: Compute stats, format metadata
    CommitDiffPanel->>FileDiffList: Render with files array
    FileDiffList->>FileDiffList: Virtualize file diffs
    FileDiffList->>LightweightDiffViewer: Render per-file diff
    LightweightDiffViewer->>CodeHighlighter: highlightCode(hunks, language)
    CodeHighlighter-->>LightweightDiffViewer: Highlighted ranges with inner-diff
    LightweightDiffViewer-->>FileDiffList: Rendered diff row
    FileDiffList-->>CommitDiffPanel: Scrollable file diff list
    CommitDiffPanel-->>BrowsePage: Full commit diff view
    BrowsePage-->>User: Display commit with stats & file diffs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #365: Modifies browse path parsing/routing utilities (getBrowseParamsFromPathParam, getBrowsePath) with overlapping path type and serialization logic.
  • PR #1146: Updates getDiffToolComponent.tsx to use the new truncateSha helper for commit SHA display.
  • PR #979: Modifies app/layout.tsx to add react-grab script loading in development, with this PR further gating it via DEBUG_ENABLE_REACT_GRAP flag.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title '[wip] Commit diffs' is vague and generic, using non-descriptive terms like '[wip]' (work-in-progress) that don't clearly convey the specific technical change, making it unclear to reviewers scanning commit history. Replace with a more descriptive title that clearly summarizes the main feature being added, such as 'Add commit diff viewer panel with file-level diff display' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bkellam/commit-diffs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

License Audit

⚠️ Status: PASS

Metric Count
Total packages 2065
Resolved (non-standard) 11
Unresolved 0
Strong copyleft 0
Weak copyleft 39

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
dompurify 3.4.0 (MPL-2.0 OR Apache-2.0)
lightningcss 1.32.0 MPL-2.0
lightningcss-android-arm64 1.32.0 MPL-2.0
lightningcss-darwin-arm64 1.32.0 MPL-2.0
lightningcss-darwin-x64 1.32.0 MPL-2.0
lightningcss-freebsd-x64 1.32.0 MPL-2.0
lightningcss-linux-arm-gnueabihf 1.32.0 MPL-2.0
lightningcss-linux-arm64-gnu 1.32.0 MPL-2.0
lightningcss-linux-arm64-musl 1.32.0 MPL-2.0
lightningcss-linux-x64-gnu 1.32.0 MPL-2.0
lightningcss-linux-x64-musl 1.32.0 MPL-2.0
lightningcss-win32-arm64-msvc 1.32.0 MPL-2.0
lightningcss-win32-x64-msvc 1.32.0 MPL-2.0
Resolved Packages (11)
Package Version Original Resolved Source
@react-grab/cli 0.1.23 UNKNOWN MIT GitHub repo https://github.com/aidenybai/react-grab — LICENSE file is MIT
@react-grab/cli 0.1.29 UNKNOWN MIT GitHub repo https://github.com/aidenybai/react-grab — LICENSE file is MIT
@react-grab/mcp 0.1.29 UNKNOWN MIT GitHub repo https://github.com/aidenybai/react-grab — README states MIT-licensed
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 GitHub repo https://github.com/livebook-dev/codemirror-lang-elixir — LICENSE file is Apache-2.0
element-source 0.0.3 UNKNOWN MIT GitHub repo https://github.com/aidenybai/element-source — repo states MIT license
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 GitHub repo https://github.com/livebook-dev/lezer-elixir — LICENSE file is Apache-2.0
map-stream 0.1.0 UNKNOWN MIT GitHub repo https://github.com/dominictarr/map-stream — LICENCE file is MIT
memorystream 0.3.1 UNKNOWN MIT npm registry — license stored as object {"type":"MIT","url":"..."}, extracted type field
pause-stream 0.0.11 ["MIT","Apache2"] MIT AND Apache-2.0 npm registry — dual-license list ["MIT", "Apache2"]; both are permissive licenses
posthog-js 1.369.0 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo https://github.com/PostHog/posthog-js — LICENSE file is Apache-2.0
valid-url 1.0.9 UNKNOWN MIT GitHub repo https://github.com/ogt/valid-url — LICENSE file is MIT

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
packages/web/src/app/layout.tsx (1)

47-59: Consider extracting the repeated debug predicate.

Same condition is duplicated for both scripts; a shared local constant keeps this easier to maintain.

Refactor sketch
 export default function RootLayout({
     children,
 }: Readonly<{
     children: React.ReactNode;
 }>) {
+    const isReactGrabDebugEnabled =
+        env.NODE_ENV === "development" && env.DEBUG_ENABLE_REACT_GRAP === "true";
+
     return (
         <html
@@
-        {env.NODE_ENV === "development" && env.DEBUG_ENABLE_REACT_GRAP === 'true' && (
+        {isReactGrabDebugEnabled && (
           <Script
             src="//unpkg.com/react-grab/dist/index.global.js"
             crossOrigin="anonymous"
             strategy="beforeInteractive"
           />
         )}
-        {env.NODE_ENV === "development" && env.DEBUG_ENABLE_REACT_GRAP === 'true' && (
+        {isReactGrabDebugEnabled && (
           <Script
             src="//unpkg.com/@react-grab/mcp/dist/client.global.js"
             strategy="lazyOnload"
           />
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/layout.tsx` around lines 47 - 59, Extract the duplicated
predicate into a single local constant (e.g. isDevReactGrabEnabled) inside the
component in layout.tsx and replace both occurrences of env.NODE_ENV ===
"development" && env.DEBUG_ENABLE_REACT_GRAP === 'true' with that constant;
ensure the constant is computed before the JSX and used to gate both <Script ...
/> renderings so the behavior is unchanged but maintenance is easier.
packages/web/src/lib/codeHighlight.ts (1)

38-50: Cache parser lookup to avoid per-line resolution overhead.

highlightCode resolves parser every call (Line 50), and it’s invoked per line in packages/web/src/app/(app)/components/lightweightCodeHighlighter.tsx (Line 60-77). A small cache avoids repeated language matching/loading work.

Proposed refactor
+const parserByLanguageNameCache = new Map<string, Promise<Parser>>();
+
 export const getCodeParserByLanguageName = async (languageName: string): Promise<Parser> => {
-    if (!languageName) {
-        return plainTextLanguage.parser;
-    }
-    const found = LanguageDescription.matchLanguageName(builtinLanguages, languageName, true);
-    if (!found) {
-        return plainTextLanguage.parser;
-    }
-    if (!found.support) {
-        try {
-            await found.load();
-        } catch {
-            return plainTextLanguage.parser;
-        }
-    }
-    return found.support ? found.support.language.parser : plainTextLanguage.parser;
+    const key = languageName?.toLowerCase() ?? '';
+    if (!parserByLanguageNameCache.has(key)) {
+        parserByLanguageNameCache.set(
+            key,
+            (async () => {
+                if (!languageName) {
+                    return plainTextLanguage.parser;
+                }
+                const found = LanguageDescription.matchLanguageName(builtinLanguages, languageName, true);
+                if (!found) {
+                    return plainTextLanguage.parser;
+                }
+                if (!found.support) {
+                    try {
+                        await found.load();
+                    } catch {
+                        return plainTextLanguage.parser;
+                    }
+                }
+                return found.support ? found.support.language.parser : plainTextLanguage.parser;
+            })(),
+        );
+    }
+    return parserByLanguageNameCache.get(key)!;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/codeHighlight.ts` around lines 38 - 50, highlightCode
currently calls getCodeParserByLanguageName on every invocation (causing heavy
per-line overhead when invoked from lightweightCodeHighlighter), so introduce a
simple module-level cache (e.g., a Map<string, Parser>) and use it inside
highlightCode to return a cached parser if present; if missing, call
getCodeParserByLanguageName, store the result in the cache, then proceed as
before. Update references to highlightCode and ensure the cache key is the
languageName string and the behavior unchanged for callers such as
lightweightCodeHighlighter; no external API changes required.
packages/web/src/app/(app)/components/pathHeader.tsx (1)

118-118: Consider replacing the hard-coded width reserve with a named constant.

40 works, but this is easy to drift as button/icon spacing evolves. A named constant (or measured control width) would make future tweaks safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/`(app)/components/pathHeader.tsx at line 118, Replace
the magic number 40 used when computing availableWidth (const availableWidth =
containerWidth - 40) with a named constant (e.g., CONTROL_RESERVE_WIDTH or
COPY_BUTTON_RESERVED_WIDTH) declared near the top of the PathHeader component or
module; update the subtraction to use that constant and adjust the comment to
explain what the reserved width covers (copy button, padding, icon spacing), or
alternatively compute the reserve by measuring the control's offsetWidth if
dynamic sizing is needed—this makes future spacing changes safer and easier to
find.
packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitRow.tsx (1)

62-67: Consider adding error handling for clipboard operations.

The clipboard write could fail (e.g., due to permissions or secure context requirements). Currently, errors are silently ignored.

♻️ Suggested improvement
     const onCopySha = useCallback(() => {
-        navigator.clipboard.writeText(commit.hash).then(() => {
-            toast({ description: "✅ Copied commit SHA to clipboard" });
-        });
+        navigator.clipboard.writeText(commit.hash)
+            .then(() => {
+                toast({ description: "✅ Copied commit SHA to clipboard" });
+            })
+            .catch(() => {
+                toast({ description: "❌ Failed to copy to clipboard", variant: "destructive" });
+            });
         return true;
     }, [commit.hash, toast]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitHistoryPanel/commitRow.tsx
around lines 62 - 67, The onCopySha callback currently calls
navigator.clipboard.writeText(commit.hash) without handling failures; update
onCopySha (the useCallback) to handle errors by awaiting or using .then/.catch
on navigator.clipboard.writeText(commit.hash) and on failure show a toast error
(e.g., toast({ variant: "destructive", description: "Failed to copy SHA" })) and
optionally log the error; ensure you still return a boolean and keep
dependencies [commit.hash, toast] unchanged.
packages/web/src/app/(app)/browse/[...path]/page.tsx (1)

116-145: Consider extracting the panel selection into a helper or switch statement.

The nested ternary chain for selecting which panel to render is functional but could become harder to maintain as more path types are added. A switch statement or mapping object would improve readability.

♻️ Optional refactor using a switch-like pattern
const renderPanel = () => {
    switch (browseProps.pathType) {
        case 'blob':
            return <CodePreviewPanel path={path} repoName={repoName} revisionName={revisionName} />;
        case 'commits':
            return <CommitsPanel path={path} repoName={repoName} revisionName={revisionName} page={page} author={author} since={since} until={until} />;
        case 'commit':
            return <CommitDiffPanel repoName={repoName} revisionName={revisionName} commitSha={browseProps.commitSha} path={path} />;
        case 'tree':
            return <TreePreviewPanel path={path} repoName={repoName} revisionName={revisionName} />;
    }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx around lines 116 - 145,
Extract the nested ternary into a small helper (e.g., renderPanel) that switches
on browseProps.pathType and returns the appropriate panel component; replace the
current inline ternary with a single call to renderPanel() and ensure the helper
returns CodePreviewPanel, CommitsPanel (passing page, author, since, until),
CommitDiffPanel (using browseProps.commitSha), or TreePreviewPanel as
appropriate to keep prop passing identical.
packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitHashLine.tsx (1)

18-23: Same clipboard error handling suggestion applies here.

This has the same pattern as commitRow.tsx - consider adding error handling for the clipboard operation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/commitHashLine.tsx
around lines 18 - 23, The onCopyHash handler in commitHashLine.tsx calls
navigator.clipboard.writeText(commitHash) without handling failures; update the
onCopyHash function (the useCallback named onCopyHash) to handle rejected
writes: either make it async and await writeText inside a try/catch or append a
.catch handler, and on error call toast with an error description (and
optionally log the error) while still returning an appropriate boolean; ensure
you reference navigator.clipboard.writeText, commitHash, and toast in the new
error path so users get feedback when the copy fails.
packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitDiffPanel.tsx (1)

27-27: Unused revisionName prop.

The revisionName parameter is declared in the props interface but never used in the component. Either remove it from the interface or use it if it's needed for future functionality.

♻️ If unused, remove from props
-export const CommitDiffPanel = async ({ repoName, commitSha, path }: CommitDiffPanelProps) => {
+export const CommitDiffPanel = async ({ repoName, revisionName, commitSha, path }: CommitDiffPanelProps) => {

Or if truly not needed:

 interface CommitDiffPanelProps {
     repoName: string;
-    revisionName?: string;
     commitSha: string;
     path: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/commitDiffPanel.tsx
at line 27, The props interface includes an unused property revisionName
referenced by CommitDiffPanelProps while CommitDiffPanel does not use it; remove
revisionName from the CommitDiffPanelProps interface (or, if intended, add usage
of revisionName inside the CommitDiffPanel component where
repoName/commitSha/path are used) so the prop is either consumed or eliminated —
update any call sites that pass revisionName accordingly and run type checks to
ensure no remaining references.
packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx (1)

100-107: Redundant key prop on FileDiffRow.

The key={rowKey} on FileDiffRow is unnecessary since the parent div already has key={virtualRow.key}. React only needs keys on the direct children of the mapped array.

♻️ Remove redundant key
                         <FileDiffRow
-                            key={rowKey}
                             file={file}
                             yOffset={virtualRow.start}
                             repoName={repoName}
                             commitSha={commitSha}
                             parentSha={parentSha}
                         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx
around lines 100 - 107, The FileDiffRow instance is receiving a redundant key
prop (key={rowKey}) even though its parent element uses key={virtualRow.key};
remove the unnecessary key prop from the FileDiffRow JSX in fileDiffList.tsx so
only the parent wrapper provides the key, leaving props file, yOffset, repoName,
commitSha and parentSha unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/lib/codeHighlight.ts`:
- Around line 21-24: The code calls found.load() without handling rejections
which can break highlighting; update the logic in codeHighlight.ts so the await
found.load() is wrapped with error handling (try/catch or .catch) and if loading
fails or found.support is still falsy return plainTextLanguage.parser as a safe
fallback; apply the same change to the second occurrence around lines 32–35 (the
other branch that accesses found.support and found.load()) and reference
found.load(), found.support and plainTextLanguage.parser when making the change.

In `@packages/web/src/lib/utils.ts`:
- Around line 31-35: The regex currently truncates any string starting with 40
hex chars; change it to only treat a ref as a SHA when it is exactly 40 hex
chars or when it is followed by a non-alphanumeric separator. Replace the match
logic using ref.match(/^([0-9a-f]{40})(.*)$/i) with a regex that makes the
suffix optional and only captures it if it begins with a non-alphanumeric
character (for example /^([0-9a-f]{40})([^0-9A-Za-z].*)?$/i), then return
match[1].slice(0,7) + (match[2] || '') so symbolic refs that start with 40 hex
chars but have an alphanumeric suffix are not truncated.

---

Nitpick comments:
In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/commitDiffPanel.tsx:
- Line 27: The props interface includes an unused property revisionName
referenced by CommitDiffPanelProps while CommitDiffPanel does not use it; remove
revisionName from the CommitDiffPanelProps interface (or, if intended, add usage
of revisionName inside the CommitDiffPanel component where
repoName/commitSha/path are used) so the prop is either consumed or eliminated —
update any call sites that pass revisionName accordingly and run type checks to
ensure no remaining references.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/commitHashLine.tsx:
- Around line 18-23: The onCopyHash handler in commitHashLine.tsx calls
navigator.clipboard.writeText(commitHash) without handling failures; update the
onCopyHash function (the useCallback named onCopyHash) to handle rejected
writes: either make it async and await writeText inside a try/catch or append a
.catch handler, and on error call toast with an error description (and
optionally log the error) while still returning an appropriate boolean; ensure
you reference navigator.clipboard.writeText, commitHash, and toast in the new
error path so users get feedback when the copy fails.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx:
- Around line 100-107: The FileDiffRow instance is receiving a redundant key
prop (key={rowKey}) even though its parent element uses key={virtualRow.key};
remove the unnecessary key prop from the FileDiffRow JSX in fileDiffList.tsx so
only the parent wrapper provides the key, leaving props file, yOffset, repoName,
commitSha and parentSha unchanged.

In
`@packages/web/src/app/`(app)/browse/[...path]/components/commitHistoryPanel/commitRow.tsx:
- Around line 62-67: The onCopySha callback currently calls
navigator.clipboard.writeText(commit.hash) without handling failures; update
onCopySha (the useCallback) to handle errors by awaiting or using .then/.catch
on navigator.clipboard.writeText(commit.hash) and on failure show a toast error
(e.g., toast({ variant: "destructive", description: "Failed to copy SHA" })) and
optionally log the error; ensure you still return a boolean and keep
dependencies [commit.hash, toast] unchanged.

In `@packages/web/src/app/`(app)/browse/[...path]/page.tsx:
- Around line 116-145: Extract the nested ternary into a small helper (e.g.,
renderPanel) that switches on browseProps.pathType and returns the appropriate
panel component; replace the current inline ternary with a single call to
renderPanel() and ensure the helper returns CodePreviewPanel, CommitsPanel
(passing page, author, since, until), CommitDiffPanel (using
browseProps.commitSha), or TreePreviewPanel as appropriate to keep prop passing
identical.

In `@packages/web/src/app/`(app)/components/pathHeader.tsx:
- Line 118: Replace the magic number 40 used when computing availableWidth
(const availableWidth = containerWidth - 40) with a named constant (e.g.,
CONTROL_RESERVE_WIDTH or COPY_BUTTON_RESERVED_WIDTH) declared near the top of
the PathHeader component or module; update the subtraction to use that constant
and adjust the comment to explain what the reserved width covers (copy button,
padding, icon spacing), or alternatively compute the reserve by measuring the
control's offsetWidth if dynamic sizing is needed—this makes future spacing
changes safer and easier to find.

In `@packages/web/src/app/layout.tsx`:
- Around line 47-59: Extract the duplicated predicate into a single local
constant (e.g. isDevReactGrabEnabled) inside the component in layout.tsx and
replace both occurrences of env.NODE_ENV === "development" &&
env.DEBUG_ENABLE_REACT_GRAP === 'true' with that constant; ensure the constant
is computed before the JSX and used to gate both <Script ... /> renderings so
the behavior is unchanged but maintenance is easier.

In `@packages/web/src/lib/codeHighlight.ts`:
- Around line 38-50: highlightCode currently calls getCodeParserByLanguageName
on every invocation (causing heavy per-line overhead when invoked from
lightweightCodeHighlighter), so introduce a simple module-level cache (e.g., a
Map<string, Parser>) and use it inside highlightCode to return a cached parser
if present; if missing, call getCodeParserByLanguageName, store the result in
the cache, then proceed as before. Update references to highlightCode and ensure
the cache key is the languageName string and the behavior unchanged for callers
such as lightweightCodeHighlighter; no external API changes required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 542c5684-421f-4dc6-8132-95d09095e896

📥 Commits

Reviewing files that changed from the base of the PR and between 105ddf4 and bb56039.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (36)
  • packages/shared/src/env.server.ts
  • packages/web/package.json
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/pureCodePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/rangeHighlightingExtension.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitDiffPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitHashLine.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/commitMessage.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/diffStat.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffList.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/fileDiffRow.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/hunkParser.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/lightweightDiffViewer.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitDiffPanel/splitPairing.ts
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/authorFilter.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitRow.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitsPagination.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/commitsPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/commitHistoryPanel/dateFilter.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx
  • packages/web/src/app/(app)/browse/[...path]/page.tsx
  • packages/web/src/app/(app)/browse/components/historyRow.tsx
  • packages/web/src/app/(app)/browse/hooks/useBrowseNavigation.ts
  • packages/web/src/app/(app)/browse/hooks/utils.ts
  • packages/web/src/app/(app)/components/lightweightCodeHighlighter.tsx
  • packages/web/src/app/(app)/components/pathHeader.tsx
  • packages/web/src/app/layout.tsx
  • packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx
  • packages/web/src/features/git/getFilesApi.ts
  • packages/web/src/features/git/getFolderContentsApi.ts
  • packages/web/src/features/git/getTreeApi.ts
  • packages/web/src/features/git/logger.ts
  • packages/web/src/features/git/utils.ts
  • packages/web/src/lib/codeHighlight.ts
  • packages/web/src/lib/utils.ts
💤 Files with no reviewable changes (1)
  • packages/web/src/features/git/utils.ts

Comment on lines +21 to +24
if (!found.support) {
await found.load();
}
return found.support ? found.support.language.parser : plainTextLanguage.parser;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle language-module load failures with a safe fallback.

If found.load() rejects, highlighting fails for the whole call path. Falling back to plainTextLanguage.parser preserves rendering instead of failing hard.

Proposed fix
 export const getCodeParserByLanguageName = async (languageName: string): Promise<Parser> => {
@@
-    if (!found.support) {
-        await found.load();
-    }
+    if (!found.support) {
+        try {
+            await found.load();
+        } catch {
+            return plainTextLanguage.parser;
+        }
+    }
     return found.support ? found.support.language.parser : plainTextLanguage.parser;
 };
@@
 export const getCodeParserByFilename = async (filename: string): Promise<Parser> => {
@@
-    if (!found.support) {
-        await found.load();
-    }
+    if (!found.support) {
+        try {
+            await found.load();
+        } catch {
+            return plainTextLanguage.parser;
+        }
+    }
     return found.support ? found.support.language.parser : plainTextLanguage.parser;
 };

Also applies to: 32-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/codeHighlight.ts` around lines 21 - 24, The code calls
found.load() without handling rejections which can break highlighting; update
the logic in codeHighlight.ts so the await found.load() is wrapped with error
handling (try/catch or .catch) and if loading fails or found.support is still
falsy return plainTextLanguage.parser as a safe fallback; apply the same change
to the second occurrence around lines 32–35 (the other branch that accesses
found.support and found.load()) and reference found.load(), found.support and
plainTextLanguage.parser when making the change.

Comment on lines +31 to +35
const match = ref.match(/^([0-9a-f]{40})(.*)$/i);
if (match) {
return match[1].slice(0, 7) + match[2];
}
return ref;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restrict SHA matching to true SHA refs to avoid truncating symbolic refs.

The current regex truncates any string that starts with 40 hex chars, even if it’s actually a symbolic ref with a non-operator suffix. That conflicts with the function contract.

Suggested fix
 export function truncateSha(ref: string): string {
-    const match = ref.match(/^([0-9a-f]{40})(.*)$/i);
+    const match = ref.match(/^([0-9a-f]{40})([~^].*)?$/i);
     if (match) {
-        return match[1].slice(0, 7) + match[2];
+        return match[1].slice(0, 7) + (match[2] ?? "");
     }
     return ref;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const match = ref.match(/^([0-9a-f]{40})(.*)$/i);
if (match) {
return match[1].slice(0, 7) + match[2];
}
return ref;
export function truncateSha(ref: string): string {
const match = ref.match(/^([0-9a-f]{40})([~^].*)?$/i);
if (match) {
return match[1].slice(0, 7) + (match[2] ?? "");
}
return ref;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/utils.ts` around lines 31 - 35, The regex currently
truncates any string starting with 40 hex chars; change it to only treat a ref
as a SHA when it is exactly 40 hex chars or when it is followed by a
non-alphanumeric separator. Replace the match logic using
ref.match(/^([0-9a-f]{40})(.*)$/i) with a regex that makes the suffix optional
and only captures it if it begins with a non-alphanumeric character (for example
/^([0-9a-f]{40})([^0-9A-Za-z].*)?$/i), then return match[1].slice(0,7) +
(match[2] || '') so symbolic refs that start with 40 hex chars but have an
alphanumeric suffix are not truncated.

brendan-kellam and others added 10 commits April 28, 2026 10:32
Adds a `commit` pathType to the browse routes
(`/browse/<repo>@<branch>/-/commit/<sha>[/<file>]`) that renders a
placeholder CommitDiffPanel. Refactors browse path helpers into a
discriminated `BrowseProps` union so commitSha is required only for
pathType: 'commit'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up @codemirror/merge (via react-codemirror-merge) inside
CommitDiffPanel with a static before/after demo. Adds a CodeDiff
component that owns its language extension + view ref so each pane
can reconfigure its language compartment independently.

Also gates the react-grab dev scripts behind DEBUG_ENABLE_REACT_GRAP
so they don't load on every dev page render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the CodeMirror MergeView-based commit diff rendering with a DOM
based split-view that renders directly from git's hunks, inspired by
Chrome DevTools' DiffView. Per-file editor instances and the matching
getFileSource fetches are gone — a 50-file commit drops from ~100
network requests to 0, and per-row render cost from a full editor mount
to a synchronous Lezer highlight + grid emit.

- New `LightweightDiffViewer` builds a single 2-column subgrid with
  hunk headers spanning both sides; each cell uses `subgrid` so line
  numbers, markers, and content align across all rows.
- Pure helpers split out: `hunkParser` (body string → DiffLine[]),
  `splitPairing` (DiffLine[] → SplitRow[]).
- `presentableDiff` from @codemirror/merge supplies character-level
  intra-line diff highlighting on paired modifications.
- Lezer highlight code lifted from `lightweightCodeHighlighter` into
  `lib/codeHighlight` so both files share the helper.
- Drop `react-codemirror-merge` and `commitDiffEditor`. Long lines wrap
  via `whitespace-pre-wrap break-words` + `minmax(0, 1fr)` on the grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- File path header now sticks to the top of the scroll viewport while
  scrolling through that file's diff, using the negative-yOffset trick
  to compensate for the virtualizer's translateY positioning. Same
  pattern as searchResultsPanel/fileMatchContainer.
- Lightweight diff viewer's grid uses `minmax(<min>, max-content)` for
  line-number and marker columns so they don't collapse to zero width
  when one side of the diff is entirely blank (fully-added or
  fully-deleted files), keeping the right pane aligned across files.
- Drop the column gap between left and right panes and instead draw a
  `border-r` separator on the left cells for a cleaner divider.
- Hunk header gets an optional className so the first hunk renders with
  just `border-b` (the file header above already provides the top
  border), while subsequent hunks render with `border-y` between them.
- Drop the per-row footer padding in the virtualizer; rows now sit
  flush against each other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New `DiffStat` component renders GitHub-style additions/deletions
  counts with a 5-square indicator scaled log-ish to total change size.
  Added on the right of each file row header and on the right of the
  "N files changed" subheader for the commit total. Hidden when there
  are no line-level changes (pure renames).
- Each file row gets a `CopyIconButton` next to the path (copies
  newPath, falling back to oldPath) and a `CommitActionLink` that
  opens the file at the commit. Deleted files link to the old path
  at the parent commit so the user lands on the file's last existing
  state rather than a 404.
- `repoName`, `commitSha`, and `parentSha` are plumbed from the panel
  through `FileDiffList` to `FileDiffRow` to support the new link.
- `computeChangeCounts` is memoized per file in the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nchoring

History panel rows in both the bottom panel and the commits page are now
clickable — they navigate to the matching commit diff via router.push,
with closest('button, a') short-circuit so inner action buttons keep
their own behavior. Bottom-panel history rows also highlight via
bg-accent when their commit is the one currently being viewed.

Commit diff header now uses AuthorsAvatarGroup + getCommitAuthors +
formatAuthorsText, matching latestCommitInfo and historyRow — co-authors
parsed from the commit body show up correctly.

When the URL trailing path matches one of the commit's files, that file
is moved to the top of the FileDiffList rather than scrolled to. Avoids
estimateSize-based scroll inaccuracy and works regardless of which side
of a rename the URL points to.

Lightweight diff viewer short-circuits with "Diff too large to display"
for files containing lines over 1000 chars, matching the cutoff in
lightweightCodeHighlighter.

PathHeader's breadcrumb measurement reserved 175px for "copy button and
padding"; the actual reservation needed is ~40px. Reduced so breadcrumbs
no longer collapse prematurely on wide layouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Lift `truncateSha` (was a private helper in getDiffToolComponent) to
  `lib/utils` so PathHeader can reuse it. The branch/ref display now
  renders a 40-char SHA as `abc1234`, preserving any `^` / `~N` suffix.
- Hide the `·` separator and the path's CopyIconButton when there's no
  path (repo root). Previously a dangling `·` and copy button rendered
  with nothing between them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant