diff --git a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx
index e65a9287..8ad99303 100644
--- a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx
+++ b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx
@@ -97,6 +97,23 @@ type PierreThemeConfig = {
themeType: 'light' | 'dark';
};
+const TOOL_DIFF_UNSAFE_CSS = `
+ [data-diff-header],
+ [data-diff] {
+ [data-separator] {
+ height: 24px !important;
+ }
+ }
+`;
+
+const TOOL_DIFF_METRICS = {
+ hunkLineCount: 50,
+ lineHeight: 24,
+ diffHeaderHeight: 44,
+ hunkSeparatorHeight: 24,
+ fileGap: 0,
+};
+
const usePierreThemeConfig = (): PierreThemeConfig => {
const themeSystem = useOptionalThemeSystem();
const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []);
@@ -500,16 +517,19 @@ const DialogUnifiedDiff: React.FC<{
diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx
index f01dac67..a308ee16 100644
--- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx
+++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx
@@ -670,21 +670,120 @@ interface DiffPreviewProps {
diffViewMode: DiffViewMode;
}
+const TOOL_DIFF_UNSAFE_CSS = `
+ [data-diff-header],
+ [data-diff] {
+ [data-separator] {
+ height: 24px !important;
+ }
+ }
+`;
+
+const TOOL_DIFF_METRICS = {
+ hunkLineCount: 50,
+ lineHeight: 24,
+ diffHeaderHeight: 44,
+ hunkSeparatorHeight: 24,
+ fileGap: 0,
+};
+
+type DiffPatchEntry = {
+ id: string;
+ title: string;
+ patch: string;
+};
+
+const renderPathLikeGitChanges = (path: string) => {
+ const lastSlash = path.lastIndexOf('/');
+ if (lastSlash === -1) {
+ return
{path};
+ }
+
+ const dir = path.slice(0, lastSlash);
+ const name = path.slice(lastSlash + 1);
+
+ return (
+
+
+ {dir}
+
+
+ /
+ {name}
+
+
+ );
+};
+
+const getDiffPatchEntries = (
+ metadata: Record
| undefined,
+ fallbackDiff: string,
+ currentDirectory: string,
+ isMobile: boolean,
+): DiffPatchEntry[] => {
+ const files = Array.isArray(metadata?.files) ? metadata.files : [];
+
+ const entries = files
+ .map((file, index) => {
+ if (!file || typeof file !== 'object') {
+ return null;
+ }
+
+ const record = file as { relativePath?: unknown; filePath?: unknown; diff?: unknown };
+ const patch = typeof record.diff === 'string' ? record.diff.trim() : '';
+ if (!patch) {
+ return null;
+ }
+
+ const rawPath = typeof record.relativePath === 'string'
+ ? record.relativePath
+ : typeof record.filePath === 'string'
+ ? record.filePath
+ : `File ${index + 1}`;
+
+ const title = typeof rawPath === 'string'
+ ? getRelativePath(rawPath, currentDirectory, isMobile)
+ : `File ${index + 1}`;
+
+ return {
+ id: `${title}-${index}`,
+ title,
+ patch,
+ } satisfies DiffPatchEntry;
+ })
+ .filter((entry): entry is DiffPatchEntry => entry !== null);
+
+ if (entries.length > 0) {
+ return entries;
+ }
+
+ return [
+ {
+ id: 'diff-0',
+ title: 'Diff',
+ patch: fallbackDiff,
+ },
+ ];
+};
+
const DiffPreview: React.FC = React.memo(({ diff, pierreTheme, pierreThemeType, diffViewMode }) => {
return (
@@ -894,6 +993,10 @@ const ToolExpandedContent: React.FC
= React.memo(({
const outputString = typeof rawOutput === 'string' ? rawOutput : '';
const diffContent = typeof metadata?.diff === 'string' ? (metadata.diff as string) : null;
+ const diffEntries = React.useMemo(
+ () => (diffContent ? getDiffPatchEntries(metadata, diffContent, currentDirectory, isMobile) : []),
+ [currentDirectory, diffContent, isMobile, metadata]
+ );
const writeFilePath = part.tool === 'write'
? typeof input?.filePath === 'string'
? input.filePath
@@ -1085,14 +1188,25 @@ const ToolExpandedContent: React.FC = React.memo(({
);
}
- if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffContent) {
+ if ((part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch') && diffEntries.length > 0) {
return renderScrollableBlock(
- ,
+
+ {diffEntries.map((entry) => (
+
+ {diffEntries.length > 1 ? (
+
+ {renderPathLikeGitChanges(entry.title)}
+
+ ) : null}
+
+
+ ))}
+
,
{ className: 'p-1' }
);
}
diff --git a/packages/ui/src/components/views/PierreDiffViewer.tsx b/packages/ui/src/components/views/PierreDiffViewer.tsx
index 3891f01b..ee9859e2 100644
--- a/packages/ui/src/components/views/PierreDiffViewer.tsx
+++ b/packages/ui/src/components/views/PierreDiffViewer.tsx
@@ -63,7 +63,7 @@ const WEBKIT_SCROLL_FIX_CSS = `
pre[data-interactive-line-numbers] [data-line-number] {
touch-action: manipulation;
}
- /* Match OpenCode hunk separator sizing */
+
[data-diff-header],
[data-diff] {
[data-separator] {
@@ -201,7 +201,7 @@ export const PierreDiffViewer: React.FC = ({
layout = 'fill',
}) => {
const themeContext = useOptionalThemeSystem();
-
+
const isDark = themeContext?.currentTheme.metadata.variant === 'dark';
const lightTheme = themeContext?.availableThemes.find(t => t.metadata.id === themeContext.lightThemeId) ?? getDefaultTheme(false);
const darkTheme = themeContext?.availableThemes.find(t => t.metadata.id === themeContext.darkThemeId) ?? getDefaultTheme(true);
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
index 4ad9df22..ebdc4561 100644
--- a/packages/ui/src/index.css
+++ b/packages/ui/src/index.css
@@ -584,7 +584,7 @@ html:not(.dark) .chat-scroll {
}
/* Desktop: slightly smaller diff code text */
-
+
.pierre-diff-wrapper diffs-container {
display: block;