Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FileDiffMetadata } from '@pierre/diffs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { extractHunkPatch } from '@/lib/diff/patchFileDiff';
|
||||
|
||||
type HunkAction = 'stage' | 'unstage' | 'discard';
|
||||
|
||||
interface DiffHunkActionsProps {
|
||||
patch: string;
|
||||
fileDiff: FileDiffMetadata | undefined;
|
||||
directory: string;
|
||||
filePath: string;
|
||||
staged: boolean;
|
||||
onApplied: (action: HunkAction) => void;
|
||||
}
|
||||
|
||||
export const DiffHunkActions = React.memo<DiffHunkActionsProps>(({
|
||||
patch,
|
||||
fileDiff,
|
||||
directory,
|
||||
filePath,
|
||||
staged,
|
||||
onApplied,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { git } = useRuntimeAPIs();
|
||||
const [busyKey, setBusyKey] = React.useState<string | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const hunks = fileDiff?.hunks;
|
||||
if (!hunks || hunks.length === 0 || !patch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const run = async (hunkIndex: number, action: HunkAction) => {
|
||||
const hunkPatch = extractHunkPatch(patch, hunkIndex);
|
||||
if (!hunkPatch) {
|
||||
setError(t('diffView.hunk.unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${hunkIndex}:${action}`;
|
||||
setBusyKey(key);
|
||||
setError(null);
|
||||
try {
|
||||
if (action === 'stage') {
|
||||
if (!git.stageGitHunk) throw new Error(t('diffView.hunk.unsupported'));
|
||||
await git.stageGitHunk(directory, filePath, hunkPatch);
|
||||
} else if (action === 'unstage') {
|
||||
if (!git.unstageGitHunk) throw new Error(t('diffView.hunk.unsupported'));
|
||||
await git.unstageGitHunk(directory, filePath, hunkPatch);
|
||||
} else {
|
||||
if (!git.revertGitHunk) throw new Error(t('diffView.hunk.unsupported'));
|
||||
await git.revertGitHunk(directory, filePath, hunkPatch);
|
||||
}
|
||||
onApplied(action);
|
||||
} catch (actionError) {
|
||||
setError(actionError instanceof Error ? actionError.message : String(actionError));
|
||||
} finally {
|
||||
setBusyKey((current) => (current === key ? null : current));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 border-b border-[var(--interactive-border)]/40 bg-[var(--surface-elevated)]/40 px-3 py-1.5">
|
||||
<div className="flex items-center gap-1.5 overflow-x-auto">
|
||||
<span className="typography-micro shrink-0 text-muted-foreground uppercase">
|
||||
{t('diffView.hunk.label')}
|
||||
</span>
|
||||
{hunks.map((hunk, index) => {
|
||||
const additions = hunk.additionLines;
|
||||
const deletions = hunk.deletionLines;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex shrink-0 items-center gap-1 rounded-md border border-[var(--interactive-border)]/50 bg-background/60 px-1.5 py-0.5"
|
||||
>
|
||||
<span className="typography-micro font-semibold text-muted-foreground">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{additions > 0 ? (
|
||||
<span className="typography-micro" style={{ color: 'var(--status-success)' }}>
|
||||
+{additions}
|
||||
</span>
|
||||
) : null}
|
||||
{deletions > 0 ? (
|
||||
<span className="typography-micro" style={{ color: 'var(--status-error)' }}>
|
||||
−{deletions}
|
||||
</span>
|
||||
) : null}
|
||||
{staged ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-5 gap-1 px-1.5"
|
||||
disabled={busyKey !== null}
|
||||
onClick={() => void run(index, 'unstage')}
|
||||
title={t('diffView.hunk.unstageTitle', { index: index + 1 })}
|
||||
>
|
||||
{busyKey === `${index}:unstage` ? (
|
||||
<Icon name="loader-4" className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Icon name="arrow-go-back" className="size-3" />
|
||||
)}
|
||||
{t('diffView.hunk.unstage')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-5 gap-1 px-1.5"
|
||||
disabled={busyKey !== null}
|
||||
onClick={() => void run(index, 'stage')}
|
||||
title={t('diffView.hunk.stageTitle', { index: index + 1 })}
|
||||
>
|
||||
{busyKey === `${index}:stage` ? (
|
||||
<Icon name="loader-4" className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Icon name="add" className="size-3" />
|
||||
)}
|
||||
{t('diffView.hunk.stage')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-5 gap-1 px-1.5 text-muted-foreground hover:text-[var(--status-error)]"
|
||||
disabled={busyKey !== null}
|
||||
onClick={() => void run(index, 'discard')}
|
||||
title={t('diffView.hunk.discardTitle', { index: index + 1 })}
|
||||
>
|
||||
{busyKey === `${index}:discard` ? (
|
||||
<Icon name="loader-4" className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Icon name="close" className="size-3" />
|
||||
)}
|
||||
{t('diffView.hunk.discard')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="flex items-center gap-1.5 typography-meta" style={{ color: 'var(--status-error)' }}>
|
||||
<Icon name="error-warning" className="size-3.5 shrink-0" />
|
||||
<span className={cn('min-w-0')}>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -26,6 +26,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { DiffViewToggle } from '@/components/chat/message/DiffViewToggle';
|
||||
import type { DiffViewMode } from '@/components/chat/message/types';
|
||||
import { PierreDiffViewer } from './PierreDiffViewer';
|
||||
import { DiffHunkActions } from './DiffHunkActions';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -389,51 +390,67 @@ const InlineImageDiffViewer = React.memo<InlineImageDiffViewerProps>(({
|
||||
});
|
||||
|
||||
interface InlineDiffViewerProps {
|
||||
filePath: string;
|
||||
diff: DiffData;
|
||||
renderSideBySide: boolean;
|
||||
wrapLines: boolean;
|
||||
filePath: string;
|
||||
diff: DiffData;
|
||||
renderSideBySide: boolean;
|
||||
wrapLines: boolean;
|
||||
directory: string;
|
||||
staged: boolean;
|
||||
onHunkApplied: (action: 'stage' | 'unstage' | 'discard') => void;
|
||||
}
|
||||
|
||||
const InlineDiffViewer = React.memo<InlineDiffViewerProps>(({
|
||||
filePath,
|
||||
diff,
|
||||
renderSideBySide,
|
||||
wrapLines,
|
||||
const InlineDiffViewer = React.memo<InlineDiffViewerProps>(({
|
||||
filePath,
|
||||
diff,
|
||||
renderSideBySide,
|
||||
wrapLines,
|
||||
directory,
|
||||
staged,
|
||||
onHunkApplied,
|
||||
}) => {
|
||||
const language = React.useMemo(
|
||||
() => getLanguageFromExtension(filePath) || 'text',
|
||||
[filePath]
|
||||
);
|
||||
const language = React.useMemo(
|
||||
() => getLanguageFromExtension(filePath) || 'text',
|
||||
[filePath]
|
||||
);
|
||||
|
||||
if (diff.isBinary) {
|
||||
return <BinaryDiffPlaceholder />;
|
||||
}
|
||||
if (diff.isBinary) {
|
||||
return <BinaryDiffPlaceholder />;
|
||||
}
|
||||
|
||||
if (isImageFile(filePath)) {
|
||||
return (
|
||||
if (isImageFile(filePath)) {
|
||||
return (
|
||||
<InlineImageDiffViewer
|
||||
filePath={filePath}
|
||||
diff={diff}
|
||||
renderSideBySide={renderSideBySide}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full" style={{ contain: 'layout' }}>
|
||||
<PierreDiffViewer
|
||||
original={diff.original}
|
||||
modified={diff.modified}
|
||||
fileDiff={diff.fileDiff}
|
||||
language={language}
|
||||
fileName={filePath}
|
||||
renderSideBySide={renderSideBySide}
|
||||
wrapLines={wrapLines}
|
||||
layout="inline"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full" style={{ contain: 'layout' }}>
|
||||
{diff.patch && diff.fileDiff ? (
|
||||
<DiffHunkActions
|
||||
patch={diff.patch}
|
||||
fileDiff={diff.fileDiff}
|
||||
directory={directory}
|
||||
filePath={filePath}
|
||||
staged={staged}
|
||||
onApplied={onHunkApplied}
|
||||
/>
|
||||
) : null}
|
||||
<PierreDiffViewer
|
||||
original={diff.original}
|
||||
modified={diff.modified}
|
||||
fileDiff={diff.fileDiff}
|
||||
language={language}
|
||||
fileName={filePath}
|
||||
renderSideBySide={renderSideBySide}
|
||||
wrapLines={wrapLines}
|
||||
layout="inline"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface MultiFileDiffEntryProps {
|
||||
@@ -481,6 +498,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
}, [directory, file.path])
|
||||
);
|
||||
const setDiff = useGitStore((state) => state.setDiff);
|
||||
const fetchStatus = useGitStore((state) => state.fetchStatus);
|
||||
const setDiffFileLayout = useUIStore((state) => state.setDiffFileLayout);
|
||||
|
||||
const [diffRetryNonce, setDiffRetryNonce] = React.useState(0);
|
||||
@@ -601,6 +619,13 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
handleSelect();
|
||||
}, [handleOpenChange, handleSelect, isExpanded]);
|
||||
|
||||
const handleHunkApplied = React.useCallback(() => {
|
||||
setDiffRetryNonce((nonce) => nonce + 1);
|
||||
if (directory) {
|
||||
void fetchStatus(directory, git);
|
||||
}
|
||||
}, [directory, fetchStatus, git]);
|
||||
|
||||
return (
|
||||
<div ref={setSectionRef} className="scroll-mt-9 border-b border-[var(--interactive-border)]/40 last:border-b-0">
|
||||
<div className="sticky top-0 z-10 border-b border-[var(--interactive-border)]/35 bg-[var(--surface-elevated)]/90 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--surface-elevated)]/80">
|
||||
@@ -755,6 +780,9 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
diff={diffData}
|
||||
renderSideBySide={renderSideBySide}
|
||||
wrapLines={wrapLines}
|
||||
directory={directory}
|
||||
staged={staged}
|
||||
onHunkApplied={handleHunkApplied}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user