Skip to content

Feature/edit diff#216

Merged
viper151 merged 3 commits intomainfrom
feature/edit-diff
Oct 31, 2025
Merged

Feature/edit diff#216
viper151 merged 3 commits intomainfrom
feature/edit-diff

Conversation

@viper151
Copy link
Contributor

@viper151 viper151 commented Oct 31, 2025

Summary by CodeRabbit

  • New Features

    • Added diff navigation controls (Prev/Next) in the code editor for easier traversal between changes.
    • Introduced minimap visualization for diffed files to provide quick overview of changes.
    • Enhanced Git panel to open files with full diff context.
  • Bug Fixes

    • Improved file diff accuracy by fetching current file content from disk, ensuring diffs reflect actual changes.
    • Strengthened file path handling for enhanced security.
    • Enhanced diff display for untracked and deleted files.

…l and during messaging.

Add merge view and minimap extensions to CodeMirror for enhanced code
editing capabilities. Increase Express JSON and URL-encoded payload
limits from default (100kb) to 50mb to support larger file operations
and git diffs.
Calculate token rate once per timing session instead of recalculating
on every interval tick. This prevents the simulated token count from
jumping erratically due to random value changes.

Also remove noisy console warning in websocket utility that was
cluttering development logs without providing actionable information.
…epth

Wrap markSessionAsActive, markSessionAsInactive, markSessionAsProcessing,
markSessionAsNotProcessing, and replaceTemporarySession functions in
useCallback hooks to prevent unnecessary re-renders and stabilize
function references across component lifecycle. This optimization
ensures child components receiving these callbacks won't re-render
unnecessarily when AppContent re-renders.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

Walkthrough

This PR enhances the application's diff viewing capabilities by introducing a new backend API endpoint for retrieving files with diff context, strengthening git repository validation, and refactoring frontend components to propagate diff information through the file-opening workflow. The code editor now renders diffs using merge-view with minimap support.

Changes

Cohort / File(s) Summary
Backend API and Path Handling
server/index.js, server/routes/git.js
Increased body parsing limits to 50mb; tightened file path resolution against project root; removed backup creation for file writes; added stripDiffHeaders helper; enhanced /diff endpoint to detect untracked and deleted files; introduced validateGitRepository with git rev-parse checks; added new GET /file-with-diff endpoint returning currentContent, oldContent, and file status flags.
Package Dependencies
package.json
Temporarily added @codemirror/merge, @replit/codemirror-minimap, @xterm/addon-fit, @xterm/xterm, then removed them (final state unchanged).
Code Editor Diff Rendering
src/components/CodeEditor.jsx
Replaced manual diff decorations with unifiedMergeView; added memoized minimap extension synced to diff chunks via getChunks; introduced scrollToFirstChunkExtension and diffNavigationPanel for chunk navigation; enhanced file loading to conditionally use new_string from diffInfo; improved save logging and error handling; expanded styling for diff elements and minimap.
File Opening Workflow and Diff Context
src/components/ChatInterface.jsx, src/components/GitPanel.jsx, src/components/MainContent.jsx
Added selectedProject prop to MessageComponent; refactored diff preparation to fetch current file content via API and compute oldContent by reversing edits; added handleFileOpen in GitPanel to fetch file diffs; extended git status handling for deleted and untracked files; separated expansion and file-open interactions; wired onFileOpen prop through MainContent to GitPanel.
React Utilities and Optimizations
src/App.jsx
Added useCallback import; wrapped session lifecycle helpers (markSessionAsActive, markSessionAsInactive, etc.) in useCallback; enhanced replaceTemporarySession to preserve existing session IDs.
Visual and Logging Updates
src/components/ClaudeStatus.jsx, src/utils/websocket.js
Fixed tokenRate calculation to apply consistently over time rather than re-randomizing per tick; removed console.warn log in localhost websocket handling.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant GitPanel
    participant API as Backend API
    participant CodeEditor
    participant DiffView as Merge View + Minimap

    User->>GitPanel: Click file path
    GitPanel->>API: GET /file-with-diff?filePath
    API->>API: Detect file status (untracked/deleted/tracked)
    API->>API: Compute diff via git or /dev/null
    API-->>GitPanel: {currentContent, oldContent, isDeleted, isUntracked}
    
    GitPanel->>CodeEditor: onFileOpen(filePath, diffInfo)
    CodeEditor->>DiffView: Initialize unifiedMergeView
    DiffView->>DiffView: Render old_string vs new_string
    DiffView->>DiffView: Compute and highlight chunks
    DiffView->>DiffView: Render minimap gutter
    DiffView-->>User: Display diff with navigation & minimap
    
    User->>DiffView: Navigate chunks (Prev/Next)
    DiffView->>DiffView: scrollToFirstChunkExtension activates
    DiffView-->>User: Scroll to chunk position
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • server/routes/git.js: New endpoint with git validation logic and diff detection for multiple file states; requires verification of git command output parsing and edge case handling for untracked/deleted files.
  • src/components/CodeEditor.jsx: Significant refactoring introducing merge-view extension, minimap integration, navigation panel, and new conditional file-loading logic; extensive styling additions require careful testing across light/dark modes and diff scenarios.
  • Cross-component prop threading: selectedProject and onFileOpen props flow through ChatInterface → MessageComponent and MainContent → GitPanel; verify all call sites pass correct context.
  • Async file-open workflows: Multiple new async fetch paths in ChatInterface and GitPanel; review error handling, fallback behaviors, and race condition prevention.
  • Backend path resolution: Verify that relative path resolution against project root prevents directory traversal attacks.

Possibly related PRs

Poem

🐰 Hops through diffs with merge-view pride,
Old and new side-by-side with minimap guide,
Files now open with context so true,
Git knows deleted, untracked—nothing new!
A rabbit's refactor, polished and bright.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Feature/edit diff" directly corresponds to the primary objective of this pull request, which is implementing enhanced diff viewing and editing capabilities across the application. The changeset comprehensively addresses this feature through multiple interconnected changes: adding merge-view and minimap support in CodeEditor, enhancing diff logic in ChatInterface and GitPanel, introducing a new /file-with-diff endpoint, and strengthening backend file path handling. The title clearly communicates the main feature being introduced, despite using a "Feature/" prefix that is more typical of branch naming conventions than traditional PR titles.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/edit-diff

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.

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/index.js (1)

412-419: Block symlink traversal before reading/writing files.

The new path.resolve(...).startsWith(normalizedRoot) guard can be bypassed via symlinks. If a symlink inside the project points outside (e.g. projectRoot/link -> /etc), a request for link/passwd resolves to …/projectRoot/link/passwd, so the string check passes even though fs.readFile/writeFile operates on /etc/passwd. Similar prefix logic has been exploited in other Node servers (see CVE-2025-58751 for sirv) because it only compares strings rather than the canonical target.(miggo.io)

Please resolve the absolute real path before validating, e.g.:

const resolved = await fs.promises.realpath(
  path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath)
);
const normalizedRoot = (await fs.promises.realpath(projectRoot)) + path.sep;
if (!resolved.startsWith(normalizedRoot)) {  deny  }

and apply the same fix to both the GET and PUT endpoints.

Also applies to: 511-518

src/components/ChatInterface.jsx (1)

467-526: Refactor: Extract duplicate async file-fetching logic.

Lines 468-492 and 503-525 contain nearly identical async file-fetching logic. This duplication makes maintenance harder and increases the risk of inconsistencies.

Suggestion: Extract a shared helper function:

const fetchFileForDiff = async (filePath, isNewFile = false) => {
  if (!onFileOpen || !selectedProject?.name) return;
  
  try {
    const response = await api.readFile(selectedProject.name, filePath);
    const data = await response.json();
    
    const newContent = (response.ok && !data.error) 
      ? data.content || '' 
      : input.content || '';
    
    onFileOpen(filePath, {
      old_string: isNewFile ? '' : undefined, // Let caller specify old_string for edits
      new_string: newContent
    });
  } catch (error) {
    console.error('Error preparing diff:', error);
    if (isNewFile) {
      onFileOpen(filePath, { old_string: '', new_string: input.content || '' });
    } else {
      onFileOpen(filePath);
    }
  }
};

Then use: onClick={() => fetchFileForDiff(input.file_path, true)}

This also applies to the duplicated logic in lines 924-943 and 962-981.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eda89ef and 53c1af3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • package.json (2 hunks)
  • server/index.js (3 hunks)
  • server/routes/git.js (2 hunks)
  • src/App.jsx (3 hunks)
  • src/components/ChatInterface.jsx (10 hunks)
  • src/components/ClaudeStatus.jsx (1 hunks)
  • src/components/CodeEditor.jsx (11 hunks)
  • src/components/GitPanel.jsx (5 hunks)
  • src/components/MainContent.jsx (1 hunks)
  • src/utils/websocket.js (0 hunks)
💤 Files with no reviewable changes (1)
  • src/utils/websocket.js
🧰 Additional context used
🧬 Code graph analysis (5)
server/index.js (4)
server/routes/git.js (1)
  • filePath (476-476)
server/utils/commandParser.js (1)
  • filePath (126-126)
server/projects.js (1)
  • filePath (103-103)
server/routes/taskmaster.js (6)
  • filePath (141-141)
  • filePath (727-727)
  • filePath (801-801)
  • filePath (869-869)
  • filePath (934-934)
  • filePath (1897-1897)
src/components/CodeEditor.jsx (2)
src/contexts/ThemeContext.jsx (1)
  • isDarkMode (15-28)
src/utils/api.js (2)
  • api (23-141)
  • api (23-141)
src/components/MainContent.jsx (2)
src/App.jsx (2)
  • selectedProject (49-49)
  • isMobile (52-52)
src/components/GitPanel.jsx (1)
  • handleFileOpen (423-451)
src/components/ChatInterface.jsx (2)
src/utils/api.js (4)
  • api (23-141)
  • api (23-141)
  • authenticatedFetch (2-20)
  • authenticatedFetch (2-20)
src/App.jsx (1)
  • selectedProject (49-49)
src/components/GitPanel.jsx (2)
src/components/MainContent.jsx (1)
  • handleFileOpen (117-126)
src/utils/api.js (2)
  • authenticatedFetch (2-20)
  • authenticatedFetch (2-20)
🔇 Additional comments (8)
package.json (1)

49-82: Summary inconsistency: dependencies show as present in final state.

The AI-generated summary claims the four CodeMirror/xterm packages were "added then removed" with only ws persisting, but the code clearly shows @codemirror/merge, @replit/codemirror-minimap, @xterm/addon-fit, and @xterm/xterm all present in lines 49, 51, 55, and 57.

The added dependencies are well-aligned with the PR objectives (diff viewing with merge-view and minimap support). The versions use standard caret ranges and appear reasonable.

src/components/ClaudeStatus.jsx (1)

10-29: LGTM! Improved token progression behavior.

The refactor to calculate tokenRate once per loading session (rather than recalculating every second) results in smoother, more predictable token count progression. This improves the user experience and slightly reduces computational overhead.

src/App.jsx (4)

21-21: Good addition to support performance optimizations.

The useCallback import is necessary for the function memoization applied to session lifecycle helpers below.


185-188: LGTM: Cleaner conditional logic.

Removing the else-branch and verbose console logs simplifies the code while preserving the correct behavior—messages are only reloaded when the session is not active.


481-516: Excellent performance optimization with useCallback.

Wrapping the session lifecycle helpers (markSessionAsActive, markSessionAsInactive, markSessionAsProcessing, markSessionAsNotProcessing) in useCallback with empty dependency arrays is correct and beneficial:

  • All functions use the setState function updater form, so they don't require external dependencies
  • Stabilizes function references passed to MainContent (lines 717-720)
  • Prevents unnecessary re-renders when these callbacks haven't changed

521-535: LGTM: Enhanced robustness with useCallback.

The replaceTemporarySession function now explicitly preserves all non-temporary session IDs before adding the real session ID. This is more robust than the previous approach and prevents accidentally clearing other active sessions during the temporary-to-real session ID transition.

The empty dependency array is correct since the function uses the setState updater form.

src/components/ChatInterface.jsx (2)

173-173: LGTM: Clean prop propagation for selectedProject.

The addition of selectedProject to MessageComponent and its proper threading through the component hierarchy enables the async file-fetching logic for diff views. The implementation is clean and follows React best practices.

Also applies to: 4092-4092


923-982: Verify the new /api/git/file-with-diff endpoint exists.

The endpoint is confirmed to exist at server/routes/git.js:197, so this git-based diffing approach is properly implemented. This is a solid improvement over the naive string replacement used in the Edit tool.

Code duplication between lines 924-943 and 962-981 — Extract this identical logic into a helper function or utility to follow DRY principles. Better yet, replace the Edit tool button logic (lines 318-386) to use this same git-based endpoint instead of regex-based string replacement, ensuring consistent behavior across all file buttons.

Comment on lines +318 to 349
<button
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
onFileOpen && onFileOpen(input.file_path, {
old_string: input.old_string,
new_string: input.new_string
});
if (!onFileOpen) return;

try {
// Fetch the current file (after the edit)
const response = await api.readFile(selectedProject?.name, input.file_path);
const data = await response.json();

if (!response.ok || data.error) {
console.error('Failed to fetch file:', data.error);
onFileOpen(input.file_path);
return;
}

const currentContent = data.content || '';

// Reverse apply the edit: replace new_string back to old_string to get the file BEFORE the edit
const oldContent = currentContent.replace(input.new_string, input.old_string);

// Pass the full file before and after the edit
onFileOpen(input.file_path, {
old_string: oldContent,
new_string: currentContent
});
} catch (error) {
console.error('Error preparing diff:', error);
onFileOpen(input.file_path);
}
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Naive string replacement breaks diff calculation.

The reverse-apply logic on lines 338 and 375 has several critical flaws:

  1. String.replace() only replaces the first occurrence - if input.new_string appears multiple times in the file, only the first match is reversed, producing an incorrect diff.
  2. No validation - if input.new_string isn't found (e.g., file was modified after the edit), replace() silently returns the original string unchanged, and the diff will show the entire file as "old" content.
  3. Race condition - the file might have changed between tool execution and button click, making the diff stale or incorrect.

Fix: Use a more robust approach:

-                                  // Reverse apply the edit: replace new_string back to old_string to get the file BEFORE the edit
-                                  const oldContent = currentContent.replace(input.new_string, input.old_string);
+                                  // Reverse apply the edit: replace new_string back to old_string
+                                  const newStringIndex = currentContent.indexOf(input.new_string);
+                                  if (newStringIndex === -1) {
+                                    console.error('Could not reverse edit: new_string not found in current file');
+                                    onFileOpen(input.file_path);
+                                    return;
+                                  }
+                                  const oldContent = 
+                                    currentContent.slice(0, newStringIndex) + 
+                                    input.old_string + 
+                                    currentContent.slice(newStringIndex + input.new_string.length);

Alternatively, call the new /api/git/file-with-diff endpoint (used in lines 929, 967) here as well, which appears to provide proper git-based diffing.

Also applies to: 358-386

🤖 Prompt for AI Agents
In src/components/ChatInterface.jsx around lines 318-349 and also where similar
logic appears at 358-386, the current reverse-apply logic uses String.replace
which only replaces the first occurrence, has no validation if the new string
isn't found, and is susceptible to race conditions; replace this whole block
with a call to the existing /api/git/file-with-diff endpoint (same one used at
lines ~929/967) to retrieve a git-accurate {before, after} payload and pass
those into onFileOpen; if the endpoint call fails, fall back to fetching the
current file and compute a validated diff (find the exact location of
input.new_string using robust index-based search or a diff library, verify it
was found before substituting, and log and fallback to onFileOpen(path) when
validation fails) so you never silently return an incorrect full-file "old"
content.

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