c79d138c45
* feat(backend): add support for fetching file content at git ref Add GetFileContentAtRef method to WorkspaceTracker for retrieving file content at specific git refs (branches, commits, HEAD). This enables diff expansion by fetching old file versions without checking out. Changes: - Extract maxFileSize constant to package level (10MB) - Add GetFileContentAtRef to workspace_files.go using git show - Add workspace.file.get_at_ref WebSocket action and handler - Add agentctl client method for the new endpoint - Register handler in workspace file handlers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(web): add diff expansion support for code review Add lazy-loaded diff expansion to show more context in code reviews. When expansion is enabled, users can expand hunks to see more surrounding code by clicking expand buttons. Full file content is fetched on-demand via WebSocket. Changes: - Add useExpandableDiff hook for lazy-loading old/new file content - Add requestFileContentAtRef to workspace-files.ts - Integrate expansion into diff viewer components - Add enableExpansion and baseRef props to DiffViewer - Auto-load expansion content when enabled - Pass expansion state through diff viewer hierarchy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(web): gracefully handle expansion props for Monaco diff viewer Monaco diff viewer has built-in expansion that doesn't require fetching old file content. Strip pierre-diffs-only expansion props (enableExpansion, baseRef) when Monaco is selected to avoid passing unsupported props. This ensures the feature works correctly regardless of which diff viewer the user selects in settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(backend): add diff expansion e2e scenario and fix unstaged diff base ref - Add scenarioDiffExpansionSetup mock-agent scenario that creates a 200-line file, commits it, then modifies lines 50 and 150 to produce a two-hunk uncommitted diff for testing diff expansion in the UI - Add workspace_file_handlers for workspace.file.get_at_ref WS action - Fix enrichWithDiffData to always diff against HEAD for unstaged/staged content so locally committed files show correct diffs - Add unit tests for GetFileContentAtRef (6 sub-tests) * feat(web): add diff expansion UI and e2e test - Wire useExpandableDiff hook into FileDiffViewer and ReviewDiffList with enableExpansion and baseRef props - Gracefully handle expansion props for Monaco diff viewer fallback - Add e2e Playwright test for diff expansion between hunks * fix(web): fix diff expansion by correcting hunk separators and line splitting Three bugs prevented @pierre/diffs expand buttons from rendering: - hunkSeparators was "simple" (no expand UI); changed to "line-info" when expansion enabled - expandUnchanged:true showed all lines expanded (no separators); removed the option - .split("\n") stripped trailing newlines; now uses SPLIT_WITH_NEWLINES regex to match library * feat(web): add editor provider settings UI Add an "Editor Providers" section to the Editors settings page with Select dropdowns for each configurable context (Code Editor, Diff Viewer, Chat Code Blocks, Chat Diff Blocks). Reads from and writes to the existing localStorage-backed editor resolver store. Changes apply immediately without requiring a save action. * test(web): add e2e tests for editor provider settings Verify that the editor provider dropdowns render with correct defaults, update on selection, and persist changes across page reloads via localStorage. * feat(web): add expand/collapse unchanged regions toggle to diff toolbar Refactor diff header toolbar into composable ToolbarButtons component and add a toggle button to expand or collapse unchanged regions in both Pierre Diffs and Monaco diff viewers. Pass enableExpansion through to Monaco to control hideUnchangedRegions. * fix(web): handle deleted files in diff expansion Treat "file not found" / "no such file" errors as empty content instead of throwing, so diff expansion works correctly for deleted files. * chore(web): add diff expansion debug e2e helpers * test(web): split diff expansion tests by provider (Pierre Diffs + Monaco) Restructure diff-expansion e2e tests into two describe blocks, one per diff viewer provider. Each block sets the provider via localStorage before navigation and verifies the correct viewer renders: - Pierre Diffs: checks for diffs-container element and shadow DOM expand buttons between hunks - Monaco: checks for .monaco-diff-viewer wrapper and the built-in hideUnchangedRegions widget with "Show Unchanged Region" link Both suites verify that clicking expand reveals collapsed middle lines. * fix(web): fix Pierre Diffs expand button selectors in e2e tests Use shadow DOM data attributes ([data-expand-button], [data-expand-up]) instead of XPath parent traversal which doesn't work inside shadow DOM. Wait for expand buttons to load asynchronously before clicking. * fix(e2e): make diff-expansion-setup scenario idempotent The mock-agent scenario now removes and re-commits the test file before each run, so subsequent tests in the same repo don't fail with "git commit failed" when the file already exists. Also increase test timeouts to match other heavy e2e tests. * refactor(web): remove editor provider settings UI, enable expand-all in review Remove the editor provider switching UI and its tests — Monaco's diff expansion doesn't work and exposing provider switching adds confusion. Show Pierre Diffs header toolbar in the review panel (remove hideHeader) so the expand-all button is accessible. Add e2e test for expand-all. * style(web): fix prettier formatting and lint warnings Run prettier on files with formatting drift. Extract ToolbarBtn helper in diff-header-toolbar to stay within max-lines-per-function limit. Consolidate useMonacoDiffViewerState to reduce line count. * chore(web): remove debug scaffolding, revert Monaco changes, drop stray console.log Revert Monaco diff-viewer-toolbar and monaco-diff-viewer to main (expansion not functional with Monaco). Remove debug-expansion screenshot scripts and spec. Strip console.log from commit-detail-panel. Strip enableExpansion from Monaco path in diff-viewer-resolver. * fix(web): use commit-relative base ref for diff expansion Use `${commitSha}^` instead of hardcoded `HEAD~1` so expansion fetches the correct old content when viewing non-HEAD commits. * fix(web,backend): address CodeRabbit review feedback - Add aria-label to icon-only toolbar buttons for screen reader access - Guard auto-load expansion effect against error state to prevent retry loops when WebSocket fetch fails - Invalidate expansion cache on file/ref change with request versioning to prevent stale content - Preflight blob size with git cat-file -s before git show to avoid memory spikes on large files --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { useCallback, useMemo, type ReactNode } from "react";
|
|
import { useTheme } from "next-themes";
|
|
import type { FileDiffOptions, SelectedLineRange, RenderHeaderMetadataProps } from "@pierre/diffs";
|
|
import { IconPlus } from "@tabler/icons-react";
|
|
import { FONT } from "@/lib/theme/colors";
|
|
import { useGlobalViewMode } from "@/hooks/use-global-view-mode";
|
|
import { useDiffHeaderToolbar } from "./diff-header-toolbar";
|
|
import type { AnnotationMetadata } from "./use-diff-annotation-renderer";
|
|
|
|
/** CSS overrides for the Pierre diff viewer, injected via unsafeCSS. */
|
|
const DIFF_UNSAFE_CSS = `
|
|
[data-diffs],
|
|
pre[data-diffs] {
|
|
background-color: var(--background) !important;
|
|
--diffs-bg: var(--background) !important;
|
|
--diffs-bg-context: var(--background) !important;
|
|
--diffs-bg-buffer: var(--card) !important;
|
|
--diffs-bg-separator: var(--card) !important;
|
|
--diffs-bg-hover: var(--muted) !important;
|
|
--diffs-fg: var(--foreground) !important;
|
|
--diffs-fg-number: var(--muted-foreground) !important;
|
|
--diffs-addition-color-override: rgb(var(--git-addition)) !important;
|
|
--diffs-deletion-color-override: rgb(var(--git-deletion)) !important;
|
|
--diffs-bg-addition: rgb(var(--git-addition) / 0.1) !important;
|
|
--diffs-bg-deletion: rgb(var(--git-deletion) / 0.1) !important;
|
|
--diffs-bg-addition-number: rgb(var(--git-addition) / 0.15) !important;
|
|
--diffs-bg-deletion-number: rgb(var(--git-deletion) / 0.15) !important;
|
|
--diffs-bg-addition-emphasis: rgb(var(--git-addition) / 0.3) !important;
|
|
--diffs-bg-deletion-emphasis: rgb(var(--git-deletion) / 0.3) !important;
|
|
--diffs-line-height: 24px !important;
|
|
--diffs-font-size: ${FONT.size}px !important;
|
|
--diffs-font-family: ${FONT.mono} !important;
|
|
--diffs-gap-fallback: 0;
|
|
font-size: ${FONT.size}px !important;
|
|
font-family: ${FONT.mono} !important;
|
|
}
|
|
[data-line] {
|
|
min-height: 24px !important;
|
|
line-height: 24px !important;
|
|
}
|
|
[data-separator='metadata'],
|
|
[data-separator]:empty {
|
|
height: 24px !important;
|
|
background-image: repeating-linear-gradient(-45deg, transparent, transparent calc(3px * 1.414), var(--diffs-bg-buffer) calc(3px * 1.414), var(--diffs-bg-buffer) calc(4px * 1.414));
|
|
background-color: transparent !important;
|
|
border-top: 1px solid var(--diffs-bg-separator) !important;
|
|
border-bottom: 1px solid var(--diffs-bg-separator) !important;
|
|
}
|
|
[data-change-icon] {
|
|
width: 12px !important;
|
|
height: 12px !important;
|
|
}
|
|
[data-diffs-header] {
|
|
padding-inline: 12px !important;
|
|
background: var(--card) !important;
|
|
}
|
|
`;
|
|
|
|
type UseDiffOptionsArgs = {
|
|
filePath: string;
|
|
diff?: string;
|
|
enableComments: boolean;
|
|
showHeader: boolean;
|
|
wordWrap: boolean;
|
|
setWordWrap: (fn: (v: boolean) => boolean) => void;
|
|
handleLineSelectionEnd: (range: SelectedLineRange | null) => void;
|
|
onLineEnter: (props: { lineType?: string; lineNumber?: number; annotationSide?: string }) => void;
|
|
onLineLeave: () => void;
|
|
onOpenFile?: (filePath: string) => void;
|
|
onRevert?: (filePath: string) => void;
|
|
/** Enable diff expansion (requires oldLines/newLines in metadata) */
|
|
enableExpansion?: boolean;
|
|
/** Number of lines to expand per click (default: 20) */
|
|
expansionLineCount?: number;
|
|
/** When true, show all lines (no separators) */
|
|
expandUnchanged?: boolean;
|
|
/** Toggle callback for expand-all button */
|
|
onToggleExpandUnchanged?: () => void;
|
|
};
|
|
|
|
type UseDiffOptionsResult = {
|
|
globalViewMode: string;
|
|
options: FileDiffOptions<AnnotationMetadata>;
|
|
renderHeaderMetadata: ((props: RenderHeaderMetadataProps) => ReactNode) | undefined;
|
|
renderHoverUtility: () => ReactNode;
|
|
};
|
|
|
|
export function useDiffOptions(args: UseDiffOptionsArgs): UseDiffOptionsResult {
|
|
const {
|
|
filePath,
|
|
diff,
|
|
enableComments,
|
|
showHeader,
|
|
wordWrap,
|
|
setWordWrap,
|
|
handleLineSelectionEnd,
|
|
onLineEnter,
|
|
onLineLeave,
|
|
onOpenFile,
|
|
onRevert,
|
|
enableExpansion = false,
|
|
expansionLineCount = 20,
|
|
expandUnchanged,
|
|
onToggleExpandUnchanged,
|
|
} = args;
|
|
|
|
const { resolvedTheme } = useTheme();
|
|
const [globalViewMode, setGlobalViewMode] = useGlobalViewMode();
|
|
|
|
const toggleViewMode = useCallback(
|
|
() => setGlobalViewMode(globalViewMode === "split" ? "unified" : "split"),
|
|
[globalViewMode, setGlobalViewMode],
|
|
);
|
|
|
|
const toggleWordWrap = useCallback(() => setWordWrap((v: boolean) => !v), [setWordWrap]);
|
|
|
|
const renderHeaderMetadata = useDiffHeaderToolbar({
|
|
filePath,
|
|
diff,
|
|
wordWrap,
|
|
onToggleWordWrap: toggleWordWrap,
|
|
viewMode: globalViewMode,
|
|
onToggleViewMode: toggleViewMode,
|
|
onOpenFile,
|
|
onRevert,
|
|
expandUnchanged,
|
|
onToggleExpandUnchanged,
|
|
});
|
|
|
|
const renderHoverUtility = useCallback((): ReactNode => {
|
|
if (!enableComments) return null;
|
|
return (
|
|
<div
|
|
className="flex h-5 w-5 cursor-pointer items-center justify-center rounded border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
title="Add comment"
|
|
>
|
|
<IconPlus className="h-3 w-3" />
|
|
</div>
|
|
);
|
|
}, [enableComments]);
|
|
|
|
const options = useMemo<FileDiffOptions<AnnotationMetadata>>(() => {
|
|
return {
|
|
diffStyle: globalViewMode,
|
|
themeType: resolvedTheme === "dark" ? "dark" : "light",
|
|
enableLineSelection: enableComments,
|
|
// "line-info" shows expand buttons when oldLines/newLines are on metadata;
|
|
// "simple" is a plain divider without expand controls.
|
|
hunkSeparators: enableExpansion ? "line-info" : "simple",
|
|
enableHoverUtility: enableComments,
|
|
diffIndicators: "none",
|
|
onLineSelectionEnd: handleLineSelectionEnd,
|
|
onLineEnter,
|
|
onLineLeave,
|
|
disableFileHeader: !showHeader,
|
|
overflow: wordWrap ? "wrap" : "scroll",
|
|
unsafeCSS: DIFF_UNSAFE_CSS,
|
|
expansionLineCount,
|
|
expandUnchanged,
|
|
};
|
|
}, [
|
|
globalViewMode,
|
|
resolvedTheme,
|
|
enableComments,
|
|
showHeader,
|
|
handleLineSelectionEnd,
|
|
wordWrap,
|
|
onLineEnter,
|
|
onLineLeave,
|
|
enableExpansion,
|
|
expansionLineCount,
|
|
expandUnchanged,
|
|
]);
|
|
|
|
return {
|
|
globalViewMode,
|
|
options,
|
|
renderHeaderMetadata: showHeader ? renderHeaderMetadata : undefined,
|
|
renderHoverUtility,
|
|
};
|
|
}
|