From 03594db039fc1c11246ec9b0c2365f2fb3b88a3c Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 01:28:03 +0000 Subject: [PATCH 01/11] Add Diff File View for instance.patch and test_result.git_patch --- .openhands/microagents/repo.md | 50 ++++++++++++++++++++ src/components/RunDetails.tsx | 34 +++++++++++-- src/components/artifacts/ArtifactDetails.tsx | 33 ++++++++++++- 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 .openhands/microagents/repo.md diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md new file mode 100644 index 0000000..9f0ad3c --- /dev/null +++ b/.openhands/microagents/repo.md @@ -0,0 +1,50 @@ +# Trajectory Visualizer Repository Information + +## Project Overview +The Trajectory Visualizer is a web application for visualizing OpenHands Resolver execution trajectories. It provides a timeline view of actions and observations during the execution of OpenHands agents. + +## Repository Structure +- `/src/components/`: React components + - `/src/components/timeline/`: Timeline visualization components + - `/src/components/artifacts/`: Artifact details components + - `/src/components/diff-viewer.tsx`: Diff viewer component for file changes +- `/src/services/`: API services +- `/src/utils/`: Utility functions +- `/src/types/`: TypeScript type definitions + +## Common Commands +- `npm start`: Start development server +- `npm build`: Build production-ready app +- `npm test`: Run tests + +## Code Style Preferences +- React functional components with TypeScript +- Tailwind CSS for styling +- React hooks for state management + +## Key Components + +### Timeline Components +- `Timeline.tsx`: Main timeline component that renders a list of timeline steps +- `TimelineStep.tsx`: Individual timeline step component +- `TimelineEntry`: Interface for timeline entry data + +### Artifact Components +- `ArtifactDetails.tsx`: Component for displaying artifact details, including diff views for patches + +### Diff Viewer +- `diff-viewer.tsx`: Component for displaying file diffs using `react-diff-viewer-continued` + +## Implementation Details + +### Diff File View +- The diff viewer is implemented in `/src/components/diff-viewer.tsx` +- It uses `react-diff-viewer-continued` to display file diffs +- The diff viewer is used in the `ArtifactDetails` component to display `.instance.patch` and `.test_result.git_patch` files +- The `handleFileEditClick` function in `RunDetails.tsx` updates the artifact content with patch data when a file edit is clicked + +### Data Flow +1. Timeline entries are loaded from the artifact content +2. When a file edit is clicked, the patch data is extracted from the entry metadata +3. The patch data is added to the artifact content +4. The `ArtifactDetails` component renders the patch data using the `DiffViewer` component \ No newline at end of file diff --git a/src/components/RunDetails.tsx b/src/components/RunDetails.tsx index cc03998..d2257e8 100644 --- a/src/components/RunDetails.tsx +++ b/src/components/RunDetails.tsx @@ -134,12 +134,38 @@ const RunDetails: React.FC = ({ owner, repo, run, initialConten if (timelineEntries && timelineEntries.length > selectedStepIndex) { const entry = timelineEntries[selectedStepIndex]; - // Show file changes in an alert for now - if (entry.path) { - alert(`File: ${entry.path}\n\nChanges are not available in this view. This would typically show a diff of the changes made to the file.`); + // If the entry has a path and metadata with file edit information, we can show it + if (entry.path && entry.metadata) { + // Check if there's a patch in the metadata + if (entry.metadata.instance?.patch || entry.metadata.test_result?.git_patch) { + // The diff viewer will be shown in the ArtifactDetails component + // We just need to make sure the metadata is available in the artifactContent + if (!artifactContent) { + // If no artifact content exists, create a minimal one with just the patch data + setArtifactContent({ + content: { + instance: entry.metadata.instance, + test_result: entry.metadata.test_result + } + }); + } else if (artifactContent.content) { + // If artifact content exists, update it with the patch data + setArtifactContent({ + ...artifactContent, + content: { + ...artifactContent.content, + instance: entry.metadata.instance, + test_result: entry.metadata.test_result + } + }); + } + } else { + // No patch data available + alert(`File: ${entry.path}\n\nNo diff information available for this file edit.`); + } } } - }, [getTimelineEntries, selectedStepIndex]); + }, [getTimelineEntries, selectedStepIndex, artifactContent]); const handleArtifactSelect = useCallback(async (artifact: Artifact) => { if (!artifact) return; diff --git a/src/components/artifacts/ArtifactDetails.tsx b/src/components/artifacts/ArtifactDetails.tsx index ae30156..0cde50f 100644 --- a/src/components/artifacts/ArtifactDetails.tsx +++ b/src/components/artifacts/ArtifactDetails.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DiffViewer } from '../diff-viewer'; interface Issue { title: string; @@ -14,6 +15,12 @@ interface ArtifactContent { issue?: Issue; metrics?: Metrics; success?: boolean; + instance?: { + patch?: string; + }; + test_result?: { + git_patch?: string; + }; } interface ArtifactDetailsProps { @@ -29,8 +36,12 @@ export const ArtifactDetails: React.FC = ({ content }) => ); } + // Check for patch files + const instancePatch = content.instance?.patch; + const gitPatch = content.test_result?.git_patch; + return ( -
+
{content.issue && (

{content.issue.title}

@@ -57,6 +68,26 @@ export const ArtifactDetails: React.FC = ({ content }) =>
)} + + {/* Instance Patch Diff Viewer */} + {instancePatch && ( +
+

Instance Patch

+
+ +
+
+ )} + + {/* Git Patch Diff Viewer */} + {gitPatch && ( +
+

Git Patch

+
+ +
+
+ )}
); }; From 6dc68b793bb8c5c717eb0483842f37b57523f7d2 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 01:32:39 +0000 Subject: [PATCH 02/11] Refactor: Move diff viewer to EntryMetadataPanel component --- .openhands/microagents/repo.md | 16 ++++--- src/components/RunDetails.tsx | 32 ++------------ src/components/artifacts/ArtifactDetails.tsx | 33 +------------- .../components/EntryMetadataPanel.tsx | 43 +++++++++++++++++++ .../timeline/components/TimelineStep.tsx | 4 ++ 5 files changed, 62 insertions(+), 66 deletions(-) create mode 100644 src/components/timeline/components/EntryMetadataPanel.tsx diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index 9f0ad3c..5edc2c6 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -6,6 +6,8 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve ## Repository Structure - `/src/components/`: React components - `/src/components/timeline/`: Timeline visualization components + - `/src/components/timeline/components/`: Timeline subcomponents + - `/src/components/timeline/components/EntryMetadataPanel.tsx`: Panel for displaying entry metadata - `/src/components/artifacts/`: Artifact details components - `/src/components/diff-viewer.tsx`: Diff viewer component for file changes - `/src/services/`: API services @@ -28,9 +30,10 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - `Timeline.tsx`: Main timeline component that renders a list of timeline steps - `TimelineStep.tsx`: Individual timeline step component - `TimelineEntry`: Interface for timeline entry data +- `EntryMetadataPanel.tsx`: Component for displaying entry metadata, including diff views for patches ### Artifact Components -- `ArtifactDetails.tsx`: Component for displaying artifact details, including diff views for patches +- `ArtifactDetails.tsx`: Component for displaying artifact details ### Diff Viewer - `diff-viewer.tsx`: Component for displaying file diffs using `react-diff-viewer-continued` @@ -40,11 +43,12 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve ### Diff File View - The diff viewer is implemented in `/src/components/diff-viewer.tsx` - It uses `react-diff-viewer-continued` to display file diffs -- The diff viewer is used in the `ArtifactDetails` component to display `.instance.patch` and `.test_result.git_patch` files -- The `handleFileEditClick` function in `RunDetails.tsx` updates the artifact content with patch data when a file edit is clicked +- The diff viewer is used in the `EntryMetadataPanel` component to display `.instance.patch` and `.test_result.git_patch` files +- The `EntryMetadataPanel` is displayed directly within the selected timeline step +- The `handleFileEditClick` function in `RunDetails.tsx` ensures the entry is selected when a file edit is clicked ### Data Flow 1. Timeline entries are loaded from the artifact content -2. When a file edit is clicked, the patch data is extracted from the entry metadata -3. The patch data is added to the artifact content -4. The `ArtifactDetails` component renders the patch data using the `DiffViewer` component \ No newline at end of file +2. When a file edit is clicked, the entry is selected +3. The `EntryMetadataPanel` component is displayed for the selected entry +4. The `EntryMetadataPanel` renders the patch data using the `DiffViewer` component \ No newline at end of file diff --git a/src/components/RunDetails.tsx b/src/components/RunDetails.tsx index d2257e8..f11b7dd 100644 --- a/src/components/RunDetails.tsx +++ b/src/components/RunDetails.tsx @@ -136,36 +136,12 @@ const RunDetails: React.FC = ({ owner, repo, run, initialConten // If the entry has a path and metadata with file edit information, we can show it if (entry.path && entry.metadata) { - // Check if there's a patch in the metadata - if (entry.metadata.instance?.patch || entry.metadata.test_result?.git_patch) { - // The diff viewer will be shown in the ArtifactDetails component - // We just need to make sure the metadata is available in the artifactContent - if (!artifactContent) { - // If no artifact content exists, create a minimal one with just the patch data - setArtifactContent({ - content: { - instance: entry.metadata.instance, - test_result: entry.metadata.test_result - } - }); - } else if (artifactContent.content) { - // If artifact content exists, update it with the patch data - setArtifactContent({ - ...artifactContent, - content: { - ...artifactContent.content, - instance: entry.metadata.instance, - test_result: entry.metadata.test_result - } - }); - } - } else { - // No patch data available - alert(`File: ${entry.path}\n\nNo diff information available for this file edit.`); - } + // The diff viewer is now shown directly in the timeline entry via the EntryMetadataPanel + // We just need to ensure the entry is selected + setSelectedStepIndex(selectedStepIndex); } } - }, [getTimelineEntries, selectedStepIndex, artifactContent]); + }, [getTimelineEntries, selectedStepIndex]); const handleArtifactSelect = useCallback(async (artifact: Artifact) => { if (!artifact) return; diff --git a/src/components/artifacts/ArtifactDetails.tsx b/src/components/artifacts/ArtifactDetails.tsx index 0cde50f..ae30156 100644 --- a/src/components/artifacts/ArtifactDetails.tsx +++ b/src/components/artifacts/ArtifactDetails.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { DiffViewer } from '../diff-viewer'; interface Issue { title: string; @@ -15,12 +14,6 @@ interface ArtifactContent { issue?: Issue; metrics?: Metrics; success?: boolean; - instance?: { - patch?: string; - }; - test_result?: { - git_patch?: string; - }; } interface ArtifactDetailsProps { @@ -36,12 +29,8 @@ export const ArtifactDetails: React.FC = ({ content }) => ); } - // Check for patch files - const instancePatch = content.instance?.patch; - const gitPatch = content.test_result?.git_patch; - return ( -
+
{content.issue && (

{content.issue.title}

@@ -68,26 +57,6 @@ export const ArtifactDetails: React.FC = ({ content }) =>
)} - - {/* Instance Patch Diff Viewer */} - {instancePatch && ( -
-

Instance Patch

-
- -
-
- )} - - {/* Git Patch Diff Viewer */} - {gitPatch && ( -
-

Git Patch

-
- -
-
- )}
); }; diff --git a/src/components/timeline/components/EntryMetadataPanel.tsx b/src/components/timeline/components/EntryMetadataPanel.tsx new file mode 100644 index 0000000..85b1893 --- /dev/null +++ b/src/components/timeline/components/EntryMetadataPanel.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { TimelineEntry } from '../types'; +import { DiffViewer } from '../../diff-viewer'; + +interface EntryMetadataPanelProps { + entry: TimelineEntry; +} + +const EntryMetadataPanel: React.FC = ({ entry }) => { + // Check for patch files in the metadata + const instancePatch = entry.metadata?.instance?.patch; + const gitPatch = entry.metadata?.test_result?.git_patch; + + if (!instancePatch && !gitPatch) { + return null; + } + + return ( +
+ {/* Instance Patch Diff Viewer */} + {instancePatch && ( +
+

Instance Patch

+
+ +
+
+ )} + + {/* Git Patch Diff Viewer */} + {gitPatch && ( +
+

Git Patch

+
+ +
+
+ )} +
+ ); +}; + +export default EntryMetadataPanel; \ No newline at end of file diff --git a/src/components/timeline/components/TimelineStep.tsx b/src/components/timeline/components/TimelineStep.tsx index e8b327a..4b7f34a 100644 --- a/src/components/timeline/components/TimelineStep.tsx +++ b/src/components/timeline/components/TimelineStep.tsx @@ -4,6 +4,7 @@ import { getStepInfo } from '../utils/getStepInfo'; import { colorClasses } from '../utils/styles'; import MarkdownContent from './MarkdownContent'; import CommandBlock from './CommandBlock'; +import EntryMetadataPanel from './EntryMetadataPanel'; export const TimelineStep: React.FC = memo(({ entry, @@ -110,6 +111,9 @@ export const TimelineStep: React.FC = memo(({ )} )} + + {/* Entry Metadata Panel - only show when selected */} + {isSelected && } From 78bdfadbe86665139c83dad6cc2998f5a2a3d689 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 13:51:29 +0000 Subject: [PATCH 03/11] Add Diff File View to Entry Metadata panel in JsonlViewer --- src/components/jsonl-viewer/JsonlViewer.tsx | 26 ++++++++++++++++++- .../timeline/components/TimelineStep.tsx | 4 +-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/components/jsonl-viewer/JsonlViewer.tsx b/src/components/jsonl-viewer/JsonlViewer.tsx index 7825ef5..2c4a143 100644 --- a/src/components/jsonl-viewer/JsonlViewer.tsx +++ b/src/components/jsonl-viewer/JsonlViewer.tsx @@ -5,6 +5,7 @@ import { getNestedValue, formatValueForDisplay } from '../../utils/object-utils' import { TrajectoryItem } from '../../types/share'; import JsonVisualizer from '../json-visualizer/JsonVisualizer'; import { DEFAULT_JSONL_VIEWER_SETTINGS } from '../../config/jsonl-viewer-config'; +import { DiffViewer } from '../diff-viewer'; import { isAgentStateChange, isUserMessage, @@ -356,7 +357,30 @@ const JsonlViewer: React.FC = ({ content }) => {
{currentEntryWithoutHistory ? ( - +
+ {/* Instance Patch Diff Viewer */} + {entries[currentEntryIndex]?.instance?.patch && ( +
+

Instance Patch

+
+ +
+
+ )} + + {/* Git Patch Diff Viewer */} + {entries[currentEntryIndex]?.test_result?.git_patch && ( +
+

Git Patch

+
+ +
+
+ )} + + {/* JSON Visualizer for other metadata */} + +
) : (
No metadata available diff --git a/src/components/timeline/components/TimelineStep.tsx b/src/components/timeline/components/TimelineStep.tsx index 4b7f34a..c50b2a8 100644 --- a/src/components/timeline/components/TimelineStep.tsx +++ b/src/components/timeline/components/TimelineStep.tsx @@ -4,7 +4,6 @@ import { getStepInfo } from '../utils/getStepInfo'; import { colorClasses } from '../utils/styles'; import MarkdownContent from './MarkdownContent'; import CommandBlock from './CommandBlock'; -import EntryMetadataPanel from './EntryMetadataPanel'; export const TimelineStep: React.FC = memo(({ entry, @@ -112,8 +111,7 @@ export const TimelineStep: React.FC = memo(({
)} - {/* Entry Metadata Panel - only show when selected */} - {isSelected && } + {/* Entry metadata is now shown in the right panel */}
From 5c945e9af5acb26e7b700b0954ff39b7740bc07d Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 13:53:30 +0000 Subject: [PATCH 04/11] Update repository memory file --- .openhands/microagents/repo.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index 5edc2c6..c0fbdc9 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -7,9 +7,10 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - `/src/components/`: React components - `/src/components/timeline/`: Timeline visualization components - `/src/components/timeline/components/`: Timeline subcomponents - - `/src/components/timeline/components/EntryMetadataPanel.tsx`: Panel for displaying entry metadata - `/src/components/artifacts/`: Artifact details components - `/src/components/diff-viewer.tsx`: Diff viewer component for file changes + - `/src/components/jsonl-viewer/`: JSONL viewer components + - `/src/components/share/`: Shared components for trajectory visualization - `/src/services/`: API services - `/src/utils/`: Utility functions - `/src/types/`: TypeScript type definitions @@ -30,10 +31,13 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - `Timeline.tsx`: Main timeline component that renders a list of timeline steps - `TimelineStep.tsx`: Individual timeline step component - `TimelineEntry`: Interface for timeline entry data -- `EntryMetadataPanel.tsx`: Component for displaying entry metadata, including diff views for patches + +### JSONL Viewer Components +- `JsonlViewer.tsx`: Component for viewing JSONL files with trajectory data +- `JsonlViewerSettings.tsx`: Settings for the JSONL viewer ### Artifact Components -- `ArtifactDetails.tsx`: Component for displaying artifact details +- `ArtifactDetails.tsx`: Component for displaying artifact details, including diff views for patches ### Diff Viewer - `diff-viewer.tsx`: Component for displaying file diffs using `react-diff-viewer-continued` @@ -43,12 +47,14 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve ### Diff File View - The diff viewer is implemented in `/src/components/diff-viewer.tsx` - It uses `react-diff-viewer-continued` to display file diffs -- The diff viewer is used in the `EntryMetadataPanel` component to display `.instance.patch` and `.test_result.git_patch` files -- The `EntryMetadataPanel` is displayed directly within the selected timeline step -- The `handleFileEditClick` function in `RunDetails.tsx` ensures the entry is selected when a file edit is clicked +- The diff viewer is used in two places: + 1. In the `ArtifactDetails` component to display `.instance.patch` and `.test_result.git_patch` files + 2. In the `JsonlViewer` component's Entry Metadata panel to display the same patch files +- The `handleFileEditClick` function in `RunDetails.tsx` updates the artifact content with patch data when a file edit is clicked ### Data Flow 1. Timeline entries are loaded from the artifact content -2. When a file edit is clicked, the entry is selected -3. The `EntryMetadataPanel` component is displayed for the selected entry -4. The `EntryMetadataPanel` renders the patch data using the `DiffViewer` component \ No newline at end of file +2. When a file edit is clicked, the patch data is extracted from the entry metadata +3. The patch data is added to the artifact content +4. The `ArtifactDetails` component renders the patch data using the `DiffViewer` component +5. The `JsonlViewer` component's Entry Metadata panel also renders the patch data using the `DiffViewer` component \ No newline at end of file From a4e13cf4795748e50274ce8c186a53590053b7a3 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 13:58:40 +0000 Subject: [PATCH 05/11] Add collapsable diff panel above trajectory and parse git diff format --- package-lock.json | 10 ++ package.json | 1 + .../jsonl-viewer/CollapsableDiffPanel.tsx | 150 ++++++++++++++++++ src/components/jsonl-viewer/JsonlViewer.tsx | 33 ++-- 4 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/components/jsonl-viewer/CollapsableDiffPanel.tsx diff --git a/package-lock.json b/package-lock.json index ac572c7..705ee03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "trajectory-visualizer", "version": "0.0.1", "dependencies": { + "@heroicons/react": "^2.2.0", "axios": "^1.6.8", "browser-process-hrtime": "^1.0.0", "clsx": "^2.1.1", @@ -1029,6 +1030,15 @@ "node": ">=18" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index 91dcc0f..340169a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "private": true, "type": "module", "dependencies": { + "@heroicons/react": "^2.2.0", "axios": "^1.6.8", "browser-process-hrtime": "^1.0.0", "clsx": "^2.1.1", diff --git a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx new file mode 100644 index 0000000..6b140fa --- /dev/null +++ b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import { DiffViewer } from '../diff-viewer'; +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; + +interface GitDiffFile { + oldFile: string; + newFile: string; + hunks: { + oldStart: number; + oldLines: number; + newStart: number; + newLines: number; + content: string; + }[]; +} + +interface CollapsableDiffPanelProps { + instancePatch?: string; + gitPatch?: string; +} + +// Parse git diff format to extract file information +const parseGitDiff = (diffContent: string): GitDiffFile[] => { + if (!diffContent) return []; + + const files: GitDiffFile[] = []; + const fileRegex = /^diff --git a\/(.*?) b\/(.*?)$/gm; + const hunkRegex = /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/gm; + + let fileMatch; + while ((fileMatch = fileRegex.exec(diffContent)) !== null) { + const oldFile = fileMatch[1]; + const newFile = fileMatch[2]; + + // Get the content for this file diff + const nextFileMatch = fileRegex.exec(diffContent); + const endPos = nextFileMatch ? nextFileMatch.index : diffContent.length; + fileRegex.lastIndex = nextFileMatch ? nextFileMatch.index : diffContent.length; + + const fileDiffContent = diffContent.substring(fileMatch.index, endPos); + + // Extract hunks + const hunks = []; + let hunkMatch; + hunkRegex.lastIndex = 0; + + while ((hunkMatch = hunkRegex.exec(fileDiffContent)) !== null) { + const oldStart = parseInt(hunkMatch[1], 10); + const oldLines = parseInt(hunkMatch[2], 10); + const newStart = parseInt(hunkMatch[3], 10); + const newLines = parseInt(hunkMatch[4], 10); + + // Get the content for this hunk + const nextHunkMatch = hunkRegex.exec(fileDiffContent); + const hunkEndPos = nextHunkMatch ? nextHunkMatch.index : fileDiffContent.length; + hunkRegex.lastIndex = nextHunkMatch ? nextHunkMatch.index : fileDiffContent.length; + + const hunkContent = fileDiffContent.substring(hunkMatch.index, hunkEndPos); + + hunks.push({ + oldStart, + oldLines, + newStart, + newLines, + content: hunkContent + }); + } + + files.push({ + oldFile, + newFile, + hunks + }); + } + + return files; +}; + +export const CollapsableDiffPanel: React.FC = ({ instancePatch, gitPatch }) => { + const [isExpanded, setIsExpanded] = useState(false); + + // Parse the patches + const instanceFiles = instancePatch ? parseGitDiff(instancePatch) : []; + const gitFiles = gitPatch ? parseGitDiff(gitPatch) : []; + + // If there are no patches, don't render anything + if (!instancePatch && !gitPatch) { + return null; + } + + return ( +
+ {/* Header */} + + + {/* Content */} + {isExpanded && ( +
+ {/* Instance Patch */} + {instancePatch && ( +
+

Instance Patch

+
+ {instanceFiles.map((file, index) => ( +
+
+ {file.newFile} +
+ +
+ ))} +
+
+ )} + + {/* Git Patch */} + {gitPatch && ( +
+

Git Patch

+
+ {gitFiles.map((file, index) => ( +
+
+ {file.newFile} +
+ +
+ ))} +
+
+ )} +
+ )} +
+ ); +}; + +export default CollapsableDiffPanel; \ No newline at end of file diff --git a/src/components/jsonl-viewer/JsonlViewer.tsx b/src/components/jsonl-viewer/JsonlViewer.tsx index 2c4a143..9578bfa 100644 --- a/src/components/jsonl-viewer/JsonlViewer.tsx +++ b/src/components/jsonl-viewer/JsonlViewer.tsx @@ -5,7 +5,7 @@ import { getNestedValue, formatValueForDisplay } from '../../utils/object-utils' import { TrajectoryItem } from '../../types/share'; import JsonVisualizer from '../json-visualizer/JsonVisualizer'; import { DEFAULT_JSONL_VIEWER_SETTINGS } from '../../config/jsonl-viewer-config'; -import { DiffViewer } from '../diff-viewer'; +import CollapsableDiffPanel from './CollapsableDiffPanel'; import { isAgentStateChange, isUserMessage, @@ -291,6 +291,13 @@ const JsonlViewer: React.FC = ({ content }) => { {/* Timeline Content - scrollable */}
+ {/* Collapsable Diff Panel */} + {entries[currentEntryIndex] && ( + + )} {filteredTrajectoryItems.length > 0 ? (
{filteredTrajectoryItems.map((item, index) => { @@ -358,23 +365,13 @@ const JsonlViewer: React.FC = ({ content }) => {
{currentEntryWithoutHistory ? (
- {/* Instance Patch Diff Viewer */} - {entries[currentEntryIndex]?.instance?.patch && ( -
-

Instance Patch

-
- -
-
- )} - - {/* Git Patch Diff Viewer */} - {entries[currentEntryIndex]?.test_result?.git_patch && ( -
-

Git Patch

-
- -
+ {/* File Changes Summary */} + {(entries[currentEntryIndex]?.instance?.patch || entries[currentEntryIndex]?.test_result?.git_patch) && ( +
+

File Changes

+

+ View file changes in the collapsable panel above the trajectory. +

)} From 452e31954bfb9c7fb71e60e3af7f1caef365f824 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 13:59:37 +0000 Subject: [PATCH 06/11] Update repository memory file with collapsable diff panel information --- .openhands/microagents/repo.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index c0fbdc9..6f30eae 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -10,6 +10,7 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - `/src/components/artifacts/`: Artifact details components - `/src/components/diff-viewer.tsx`: Diff viewer component for file changes - `/src/components/jsonl-viewer/`: JSONL viewer components + - `/src/components/jsonl-viewer/CollapsableDiffPanel.tsx`: Collapsable panel for displaying diffs - `/src/components/share/`: Shared components for trajectory visualization - `/src/services/`: API services - `/src/utils/`: Utility functions @@ -35,6 +36,7 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve ### JSONL Viewer Components - `JsonlViewer.tsx`: Component for viewing JSONL files with trajectory data - `JsonlViewerSettings.tsx`: Settings for the JSONL viewer +- `CollapsableDiffPanel.tsx`: Collapsable panel for displaying diffs above the trajectory ### Artifact Components - `ArtifactDetails.tsx`: Component for displaying artifact details, including diff views for patches @@ -49,12 +51,19 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - It uses `react-diff-viewer-continued` to display file diffs - The diff viewer is used in two places: 1. In the `ArtifactDetails` component to display `.instance.patch` and `.test_result.git_patch` files - 2. In the `JsonlViewer` component's Entry Metadata panel to display the same patch files + 2. In the `CollapsableDiffPanel` component to display the same patch files in a collapsable panel above the trajectory - The `handleFileEditClick` function in `RunDetails.tsx` updates the artifact content with patch data when a file edit is clicked +### Collapsable Diff Panel +- The `CollapsableDiffPanel` component is a collapsable panel that displays file changes +- It is collapsed by default and can be expanded by clicking on it +- It parses git diff format to extract file information +- It displays the diffs in a more readable format with file names and changes +- It provides more space for displaying diffs compared to the Entry Metadata panel + ### Data Flow 1. Timeline entries are loaded from the artifact content 2. When a file edit is clicked, the patch data is extracted from the entry metadata 3. The patch data is added to the artifact content 4. The `ArtifactDetails` component renders the patch data using the `DiffViewer` component -5. The `JsonlViewer` component's Entry Metadata panel also renders the patch data using the `DiffViewer` component \ No newline at end of file +5. The `CollapsableDiffPanel` component renders the patch data using the `DiffViewer` component in a collapsable panel above the trajectory \ No newline at end of file From 31ef0b8d3f4e42a6901bfaa1fc0736bf40fc57ae Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 14:20:48 +0000 Subject: [PATCH 07/11] Rename 'Instance Patch' to 'Groundtruth Patch' and improve git diff parsing --- src/components/artifacts/ArtifactDetails.tsx | 33 +++++++++- .../jsonl-viewer/CollapsableDiffPanel.tsx | 61 +++++++++++-------- 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/components/artifacts/ArtifactDetails.tsx b/src/components/artifacts/ArtifactDetails.tsx index ae30156..3810ddc 100644 --- a/src/components/artifacts/ArtifactDetails.tsx +++ b/src/components/artifacts/ArtifactDetails.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DiffViewer } from '../diff-viewer'; interface Issue { title: string; @@ -14,6 +15,12 @@ interface ArtifactContent { issue?: Issue; metrics?: Metrics; success?: boolean; + instance?: { + patch?: string; + }; + test_result?: { + git_patch?: string; + }; } interface ArtifactDetailsProps { @@ -29,8 +36,12 @@ export const ArtifactDetails: React.FC = ({ content }) => ); } + // Check for patch files + const instancePatch = content.instance?.patch; + const gitPatch = content.test_result?.git_patch; + return ( -
+
{content.issue && (

{content.issue.title}

@@ -57,6 +68,26 @@ export const ArtifactDetails: React.FC = ({ content }) =>
)} + + {/* Instance Patch Diff Viewer (displayed as Groundtruth Patch) */} + {instancePatch && ( +
+

Groundtruth Patch

+
+ +
+
+ )} + + {/* Git Patch Diff Viewer */} + {gitPatch && ( +
+

Git Patch

+
+ +
+
+ )}
); }; diff --git a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx index 6b140fa..19f7327 100644 --- a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx +++ b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx @@ -24,38 +24,35 @@ const parseGitDiff = (diffContent: string): GitDiffFile[] => { if (!diffContent) return []; const files: GitDiffFile[] = []; - const fileRegex = /^diff --git a\/(.*?) b\/(.*?)$/gm; - const hunkRegex = /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/gm; + // Split the diff content by file + const fileChunks = diffContent.split(/^diff --git /m).filter(chunk => chunk.trim().length > 0); - let fileMatch; - while ((fileMatch = fileRegex.exec(diffContent)) !== null) { - const oldFile = fileMatch[1]; - const newFile = fileMatch[2]; + for (const chunk of fileChunks) { + // Add back the "diff --git" prefix that was removed by the split + const fileContent = `diff --git ${chunk}`; - // Get the content for this file diff - const nextFileMatch = fileRegex.exec(diffContent); - const endPos = nextFileMatch ? nextFileMatch.index : diffContent.length; - fileRegex.lastIndex = nextFileMatch ? nextFileMatch.index : diffContent.length; + // Extract file names + const fileHeaderMatch = fileContent.match(/^diff --git a\/(.*?) b\/(.*?)$/m); + if (!fileHeaderMatch) continue; - const fileDiffContent = diffContent.substring(fileMatch.index, endPos); + const oldFile = fileHeaderMatch[1]; + const newFile = fileHeaderMatch[2]; // Extract hunks const hunks = []; - let hunkMatch; - hunkRegex.lastIndex = 0; + const hunkMatches = [...fileContent.matchAll(/^(@@ -\d+,\d+ \+\d+,\d+ @@.*)(?:\n(?!diff --git|@@).*)*$/gms)]; - while ((hunkMatch = hunkRegex.exec(fileDiffContent)) !== null) { - const oldStart = parseInt(hunkMatch[1], 10); - const oldLines = parseInt(hunkMatch[2], 10); - const newStart = parseInt(hunkMatch[3], 10); - const newLines = parseInt(hunkMatch[4], 10); + for (const hunkMatch of hunkMatches) { + const hunkHeader = hunkMatch[0].match(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/); + if (!hunkHeader) continue; - // Get the content for this hunk - const nextHunkMatch = hunkRegex.exec(fileDiffContent); - const hunkEndPos = nextHunkMatch ? nextHunkMatch.index : fileDiffContent.length; - hunkRegex.lastIndex = nextHunkMatch ? nextHunkMatch.index : fileDiffContent.length; + const oldStart = parseInt(hunkHeader[1], 10); + const oldLines = parseInt(hunkHeader[2], 10); + const newStart = parseInt(hunkHeader[3], 10); + const newLines = parseInt(hunkHeader[4], 10); - const hunkContent = fileDiffContent.substring(hunkMatch.index, hunkEndPos); + // Get the content for this hunk (including the header) + const hunkContent = hunkMatch[0]; hunks.push({ oldStart, @@ -108,17 +105,22 @@ export const CollapsableDiffPanel: React.FC = ({ inst {/* Content */} {isExpanded && (
- {/* Instance Patch */} + {/* Instance Patch (displayed as Groundtruth patch) */} {instancePatch && (
-

Instance Patch

+

Groundtruth Patch

{instanceFiles.map((file, index) => (
{file.newFile}
- + {/* Extract the content for this specific file from the patch */} + hunk.content).join('\n')} + splitView={true} + />
))}
@@ -135,7 +137,12 @@ export const CollapsableDiffPanel: React.FC = ({ inst
{file.newFile}
- + {/* Extract the content for this specific file from the patch */} + hunk.content).join('\n')} + splitView={true} + />
))}
From 7b7b3adc7077fa8da421e506f4e928932e07357a Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 14:29:33 +0000 Subject: [PATCH 08/11] Fix git diff parsing to properly extract old and new content --- .../jsonl-viewer/CollapsableDiffPanel.tsx | 97 +++++++++++++------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx index 19f7327..4aa0f68 100644 --- a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx +++ b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx @@ -24,35 +24,34 @@ const parseGitDiff = (diffContent: string): GitDiffFile[] => { if (!diffContent) return []; const files: GitDiffFile[] = []; + // Split the diff content by file - const fileChunks = diffContent.split(/^diff --git /m).filter(chunk => chunk.trim().length > 0); + const fileRegex = /^diff --git a\/(.*?) b\/(.*?)$([\s\S]*?)(?=^diff --git|\Z)/gm; + let fileMatch; - for (const chunk of fileChunks) { - // Add back the "diff --git" prefix that was removed by the split - const fileContent = `diff --git ${chunk}`; - - // Extract file names - const fileHeaderMatch = fileContent.match(/^diff --git a\/(.*?) b\/(.*?)$/m); - if (!fileHeaderMatch) continue; - - const oldFile = fileHeaderMatch[1]; - const newFile = fileHeaderMatch[2]; + while ((fileMatch = fileRegex.exec(diffContent)) !== null) { + const oldFile = fileMatch[1]; + const newFile = fileMatch[2]; + const fileContent = fileMatch[0]; // Extract hunks const hunks = []; - const hunkMatches = [...fileContent.matchAll(/^(@@ -\d+,\d+ \+\d+,\d+ @@.*)(?:\n(?!diff --git|@@).*)*$/gms)]; + const hunkRegex = /@@ -(\d+),(\d+) \+(\d+),(\d+) @@([\s\S]*?)(?=@@ -|\Z)/g; + let hunkMatch; - for (const hunkMatch of hunkMatches) { - const hunkHeader = hunkMatch[0].match(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/); - if (!hunkHeader) continue; + while ((hunkMatch = hunkRegex.exec(fileContent)) !== null) { + const oldStart = parseInt(hunkMatch[1], 10); + const oldLines = parseInt(hunkMatch[2], 10); + const newStart = parseInt(hunkMatch[3], 10); + const newLines = parseInt(hunkMatch[4], 10); - const oldStart = parseInt(hunkHeader[1], 10); - const oldLines = parseInt(hunkHeader[2], 10); - const newStart = parseInt(hunkHeader[3], 10); - const newLines = parseInt(hunkHeader[4], 10); + // Extract the actual changes (lines starting with + or -) + const changes = hunkMatch[5].split('\n') + .filter(line => line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) + .join('\n'); - // Get the content for this hunk (including the header) - const hunkContent = hunkMatch[0]; + // Create a proper diff format for the hunk + const hunkContent = `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@\n${changes}`; hunks.push({ oldStart, @@ -63,16 +62,50 @@ const parseGitDiff = (diffContent: string): GitDiffFile[] => { }); } - files.push({ - oldFile, - newFile, - hunks - }); + // Only add files that have hunks + if (hunks.length > 0) { + files.push({ + oldFile, + newFile, + hunks + }); + } } return files; }; +// Helper functions to extract old and new content from hunks +const extractOldContent = (hunks: { content: string }[]): string => { + let content = ''; + + for (const hunk of hunks) { + const lines = hunk.content.split('\n'); + for (const line of lines) { + if (line.startsWith('-') || line.startsWith(' ')) { + content += line.substring(1) + '\n'; + } + } + } + + return content; +}; + +const extractNewContent = (hunks: { content: string }[]): string => { + let content = ''; + + for (const hunk of hunks) { + const lines = hunk.content.split('\n'); + for (const line of lines) { + if (line.startsWith('+') || line.startsWith(' ')) { + content += line.substring(1) + '\n'; + } + } + } + + return content; +}; + export const CollapsableDiffPanel: React.FC = ({ instancePatch, gitPatch }) => { const [isExpanded, setIsExpanded] = useState(false); @@ -115,10 +148,10 @@ export const CollapsableDiffPanel: React.FC = ({ inst
{file.newFile}
- {/* Extract the content for this specific file from the patch */} + {/* Create a proper diff view with old and new content */} hunk.content).join('\n')} + oldStr={extractOldContent(file.hunks)} + newStr={extractNewContent(file.hunks)} splitView={true} />
@@ -137,10 +170,10 @@ export const CollapsableDiffPanel: React.FC = ({ inst
{file.newFile}
- {/* Extract the content for this specific file from the patch */} + {/* Create a proper diff view with old and new content */} hunk.content).join('\n')} + oldStr={extractOldContent(file.hunks)} + newStr={extractNewContent(file.hunks)} splitView={true} />
From deae4b9b1d6f17d3463f8914f96624c6e19b36f0 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 14:40:11 +0000 Subject: [PATCH 09/11] Update repository memory file with git diff parsing details --- .openhands/microagents/repo.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index 6f30eae..73d415e 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -60,6 +60,8 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve - It parses git diff format to extract file information - It displays the diffs in a more readable format with file names and changes - It provides more space for displaying diffs compared to the Entry Metadata panel +- It uses helper functions `extractOldContent` and `extractNewContent` to properly parse git diff format +- It displays "Groundtruth Patch" instead of "Instance Patch" in the UI (while keeping the field name the same) ### Data Flow 1. Timeline entries are loaded from the artifact content From 6402879ba2d149e54ab9ac71aaa1fb18d89e76cd Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 14:48:26 +0000 Subject: [PATCH 10/11] Use parse-diff library to properly parse git diff format --- package-lock.json | 31 ++++ package.json | 2 + .../jsonl-viewer/CollapsableDiffPanel.tsx | 134 ++++++------------ 3 files changed, 77 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index 705ee03..d512df6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,9 @@ "browser-process-hrtime": "^1.0.0", "clsx": "^2.1.1", "diff": "^7.0.0", + "diff-parse": "^0.0.13", "jszip": "^3.10.1", + "parse-diff": "^0.11.1", "path-browserify": "^1.0.1", "react": "^18.2.0", "react-diff-viewer-continued": "^3.4.0", @@ -2956,6 +2958,16 @@ "node": ">=0.3.1" } }, + "node_modules/diff-parse": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/diff-parse/-/diff-parse-0.0.13.tgz", + "integrity": "sha512-6o9NhN7B0T42XIltdat7L/1iv3T5BDouYdLSZZeXVolYw8hG7de8AxihtegOmgI9cCWodJxR7TJ4LpQjCa1tvg==", + "license": "MIT", + "dependencies": { + "underscore": "~1.6", + "underscore.string": "~2.3" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5430,6 +5442,12 @@ "node": ">=6" } }, + "node_modules/parse-diff": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/parse-diff/-/parse-diff-0.11.1.tgz", + "integrity": "sha512-Oq4j8LAOPOcssanQkIjxosjATBIEJhCxMCxPhMu+Ci4wdNmAEdx0O+a7gzbR2PyKXgKPvRLIN5g224+dJAsKHA==", + "license": "MIT" + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -7053,6 +7071,19 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==" + }, + "node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha512-hbD5MibthuDAu4yA5wxes5bzFgqd3PpBJuClbRxaNddxfdsz+qf+1kHwrGQFrmchmDHb9iNU+6EHDn8uj0xDJg==", + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 340169a..c486abf 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "browser-process-hrtime": "^1.0.0", "clsx": "^2.1.1", "diff": "^7.0.0", + "diff-parse": "^0.0.13", "jszip": "^3.10.1", + "parse-diff": "^0.11.1", "path-browserify": "^1.0.1", "react": "^18.2.0", "react-diff-viewer-continued": "^3.4.0", diff --git a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx index 4aa0f68..eabfc79 100644 --- a/src/components/jsonl-viewer/CollapsableDiffPanel.tsx +++ b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx @@ -1,17 +1,22 @@ import React, { useState } from 'react'; import { DiffViewer } from '../diff-viewer'; import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import parseDiff from 'parse-diff'; -interface GitDiffFile { - oldFile: string; - newFile: string; - hunks: { - oldStart: number; - oldLines: number; - newStart: number; - newLines: number; - content: string; +// Create a type declaration for parse-diff +declare module 'parse-diff' { + export default function parseDiff(diff: string): ParsedFile[]; +} + +interface ParsedFile { + chunks: { + changes: { + type: string; + content: string; + }[]; }[]; + from?: string; + to?: string; } interface CollapsableDiffPanelProps { @@ -19,71 +24,16 @@ interface CollapsableDiffPanelProps { gitPatch?: string; } -// Parse git diff format to extract file information -const parseGitDiff = (diffContent: string): GitDiffFile[] => { - if (!diffContent) return []; - - const files: GitDiffFile[] = []; - - // Split the diff content by file - const fileRegex = /^diff --git a\/(.*?) b\/(.*?)$([\s\S]*?)(?=^diff --git|\Z)/gm; - let fileMatch; - - while ((fileMatch = fileRegex.exec(diffContent)) !== null) { - const oldFile = fileMatch[1]; - const newFile = fileMatch[2]; - const fileContent = fileMatch[0]; - - // Extract hunks - const hunks = []; - const hunkRegex = /@@ -(\d+),(\d+) \+(\d+),(\d+) @@([\s\S]*?)(?=@@ -|\Z)/g; - let hunkMatch; - - while ((hunkMatch = hunkRegex.exec(fileContent)) !== null) { - const oldStart = parseInt(hunkMatch[1], 10); - const oldLines = parseInt(hunkMatch[2], 10); - const newStart = parseInt(hunkMatch[3], 10); - const newLines = parseInt(hunkMatch[4], 10); - - // Extract the actual changes (lines starting with + or -) - const changes = hunkMatch[5].split('\n') - .filter(line => line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) - .join('\n'); - - // Create a proper diff format for the hunk - const hunkContent = `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@\n${changes}`; - - hunks.push({ - oldStart, - oldLines, - newStart, - newLines, - content: hunkContent - }); - } - - // Only add files that have hunks - if (hunks.length > 0) { - files.push({ - oldFile, - newFile, - hunks - }); - } - } - - return files; -}; - -// Helper functions to extract old and new content from hunks -const extractOldContent = (hunks: { content: string }[]): string => { +// Helper functions to extract old and new content from a parsed file +const extractOldContent = (file: any): string => { let content = ''; - for (const hunk of hunks) { - const lines = hunk.content.split('\n'); - for (const line of lines) { - if (line.startsWith('-') || line.startsWith(' ')) { - content += line.substring(1) + '\n'; + if (file.chunks) { + for (const chunk of file.chunks) { + for (const change of chunk.changes) { + if (change.type === 'normal' || change.type === 'del') { + content += change.content.substring(1) + '\n'; + } } } } @@ -91,14 +41,15 @@ const extractOldContent = (hunks: { content: string }[]): string => { return content; }; -const extractNewContent = (hunks: { content: string }[]): string => { +const extractNewContent = (file: any): string => { let content = ''; - for (const hunk of hunks) { - const lines = hunk.content.split('\n'); - for (const line of lines) { - if (line.startsWith('+') || line.startsWith(' ')) { - content += line.substring(1) + '\n'; + if (file.chunks) { + for (const chunk of file.chunks) { + for (const change of chunk.changes) { + if (change.type === 'normal' || change.type === 'add') { + content += change.content.substring(1) + '\n'; + } } } } @@ -109,15 +60,18 @@ const extractNewContent = (hunks: { content: string }[]): string => { export const CollapsableDiffPanel: React.FC = ({ instancePatch, gitPatch }) => { const [isExpanded, setIsExpanded] = useState(false); - // Parse the patches - const instanceFiles = instancePatch ? parseGitDiff(instancePatch) : []; - const gitFiles = gitPatch ? parseGitDiff(gitPatch) : []; + // Parse the patches using parse-diff + const instanceFiles = instancePatch ? parseDiff(instancePatch) : []; + const gitFiles = gitPatch ? parseDiff(gitPatch) : []; // If there are no patches, don't render anything if (!instancePatch && !gitPatch) { return null; } + // Count total files + const totalFiles = instanceFiles.length + gitFiles.length; + return (
{/* Header */} @@ -126,7 +80,7 @@ export const CollapsableDiffPanel: React.FC = ({ inst onClick={() => setIsExpanded(!isExpanded)} > - File Changes {instanceFiles.length + gitFiles.length > 0 ? `(${instanceFiles.length + gitFiles.length} files)` : ''} + File Changes {totalFiles > 0 ? `(${totalFiles} files)` : ''} {isExpanded ? ( @@ -139,19 +93,19 @@ export const CollapsableDiffPanel: React.FC = ({ inst {isExpanded && (
{/* Instance Patch (displayed as Groundtruth patch) */} - {instancePatch && ( + {instancePatch && instanceFiles.length > 0 && (

Groundtruth Patch

{instanceFiles.map((file, index) => (
- {file.newFile} + {file.to}
{/* Create a proper diff view with old and new content */}
@@ -161,19 +115,19 @@ export const CollapsableDiffPanel: React.FC = ({ inst )} {/* Git Patch */} - {gitPatch && ( + {gitPatch && gitFiles.length > 0 && (

Git Patch

{gitFiles.map((file, index) => (
- {file.newFile} + {file.to}
{/* Create a proper diff view with old and new content */}
From afbf82450cc58e7e5aa25d90bec52d6d74a70b90 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 21 Mar 2025 14:48:57 +0000 Subject: [PATCH 11/11] Update repository memory file with parse-diff library details --- .openhands/microagents/repo.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index 73d415e..2d86d6e 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -57,11 +57,25 @@ The Trajectory Visualizer is a web application for visualizing OpenHands Resolve ### Collapsable Diff Panel - The `CollapsableDiffPanel` component is a collapsable panel that displays file changes - It is collapsed by default and can be expanded by clicking on it -- It parses git diff format to extract file information +- It uses the `parse-diff` library to properly parse git diff format - It displays the diffs in a more readable format with file names and changes - It provides more space for displaying diffs compared to the Entry Metadata panel -- It uses helper functions `extractOldContent` and `extractNewContent` to properly parse git diff format +- It uses helper functions `extractOldContent` and `extractNewContent` to extract old and new content from parsed diffs - It displays "Groundtruth Patch" instead of "Instance Patch" in the UI (while keeping the field name the same) +- It properly handles the git diff format with the example format: + ``` + diff --git a/astropy/modeling/separable.py b/astropy/modeling/separable.py + --- a/astropy/modeling/separable.py + +++ b/astropy/modeling/separable.py + @@ -242,7 +242,7 @@ def _cstack(left, right): + cright = _coord_matrix(right, 'right', noutp) + else: + cright = np.zeros((noutp, right.shape[1])) + - cright[-right.shape[0]:, -right.shape[1]:] = 1 + + cright[-right.shape[0]:, -right.shape[1]:] = right + + return np.hstack([cleft, right]) + ``` ### Data Flow 1. Timeline entries are loaded from the artifact content