diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md new file mode 100644 index 0000000..2d86d6e --- /dev/null +++ b/.openhands/microagents/repo.md @@ -0,0 +1,85 @@ +# 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/timeline/components/`: Timeline subcomponents + - `/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 +- `/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 + +### 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 + +### 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 two places: + 1. In the `ArtifactDetails` component to display `.instance.patch` and `.test_result.git_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 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 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 +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 `CollapsableDiffPanel` component renders the patch data using the `DiffViewer` component in a collapsable panel above the trajectory \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ac572c7..d512df6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,14 @@ "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", "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", @@ -1029,6 +1032,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", @@ -2946,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", @@ -5420,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", @@ -7043,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 91dcc0f..c486abf 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,14 @@ "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": "^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/RunDetails.tsx b/src/components/RunDetails.tsx index cc03998..f11b7dd 100644 --- a/src/components/RunDetails.tsx +++ b/src/components/RunDetails.tsx @@ -134,9 +134,11 @@ 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) { + // 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]); 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 new file mode 100644 index 0000000..eabfc79 --- /dev/null +++ b/src/components/jsonl-viewer/CollapsableDiffPanel.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react'; +import { DiffViewer } from '../diff-viewer'; +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import parseDiff from 'parse-diff'; + +// 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 { + instancePatch?: string; + gitPatch?: string; +} + +// Helper functions to extract old and new content from a parsed file +const extractOldContent = (file: any): string => { + let content = ''; + + 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'; + } + } + } + } + + return content; +}; + +const extractNewContent = (file: any): string => { + let content = ''; + + 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'; + } + } + } + } + + return content; +}; + +export const CollapsableDiffPanel: React.FC = ({ instancePatch, gitPatch }) => { + const [isExpanded, setIsExpanded] = useState(false); + + // 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 */} + + + {/* Content */} + {isExpanded && ( +
+ {/* Instance Patch (displayed as Groundtruth patch) */} + {instancePatch && instanceFiles.length > 0 && ( +
+

Groundtruth Patch

+
+ {instanceFiles.map((file, index) => ( +
+
+ {file.to} +
+ {/* Create a proper diff view with old and new content */} + +
+ ))} +
+
+ )} + + {/* Git Patch */} + {gitPatch && gitFiles.length > 0 && ( +
+

Git Patch

+
+ {gitFiles.map((file, index) => ( +
+
+ {file.to} +
+ {/* Create a proper diff view with old and new content */} + +
+ ))} +
+
+ )} +
+ )} +
+ ); +}; + +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 7825ef5..9578bfa 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 CollapsableDiffPanel from './CollapsableDiffPanel'; import { isAgentStateChange, isUserMessage, @@ -290,6 +291,13 @@ const JsonlViewer: React.FC = ({ content }) => { {/* Timeline Content - scrollable */}
+ {/* Collapsable Diff Panel */} + {entries[currentEntryIndex] && ( + + )} {filteredTrajectoryItems.length > 0 ? (
{filteredTrajectoryItems.map((item, index) => { @@ -356,7 +364,20 @@ const JsonlViewer: React.FC = ({ content }) => {
{currentEntryWithoutHistory ? ( - +
+ {/* 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. +

+
+ )} + + {/* JSON Visualizer for other metadata */} + +
) : (
No metadata available 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..c50b2a8 100644 --- a/src/components/timeline/components/TimelineStep.tsx +++ b/src/components/timeline/components/TimelineStep.tsx @@ -110,6 +110,8 @@ export const TimelineStep: React.FC = memo(({ )}
)} + + {/* Entry metadata is now shown in the right panel */}