fix(vscode): resolve apply patch diff paths
This commit is contained in:
@@ -53,10 +53,11 @@ import {
|
|||||||
} from './taskToolModel';
|
} from './taskToolModel';
|
||||||
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { getDiffPatchEntries, getPatchText, type DiffPatchEntry } from './toolDiffUtils';
|
import { getDiffPatchEntries, getPatchText, getPrimaryToolPath, type DiffPatchEntry } from './toolDiffUtils';
|
||||||
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
|
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
|
||||||
import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle';
|
import { useStreamingTextThrottle } from '../../hooks/useStreamingTextThrottle';
|
||||||
import { getStreamingOutputAppend, getToolOutput } from './toolOutput';
|
import { getStreamingOutputAppend, getToolOutput } from './toolOutput';
|
||||||
|
import { toAbsoluteFilePath } from '@/lib/path-utils';
|
||||||
|
|
||||||
const TOOL_ROW_TEXT_CLASS = '!text-[length:var(--text-meta)] !leading-5 sm:!leading-6 tracking-normal';
|
const TOOL_ROW_TEXT_CLASS = '!text-[length:var(--text-meta)] !leading-5 sm:!leading-6 tracking-normal';
|
||||||
const TOOL_ROW_TITLE_CLASS = cn('typography-meta font-medium', TOOL_ROW_TEXT_CLASS);
|
const TOOL_ROW_TITLE_CLASS = cn('typography-meta font-medium', TOOL_ROW_TEXT_CLASS);
|
||||||
@@ -423,8 +424,10 @@ const getPrimaryDiffFromMetadata = (
|
|||||||
if (!file || typeof file !== 'object') {
|
if (!file || typeof file !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const candidate = file as { relativePath?: unknown; filePath?: unknown };
|
const candidate = file as { relativePath?: unknown; filePath?: unknown; movePath?: unknown };
|
||||||
return candidate.relativePath === preferred || candidate.filePath === preferred;
|
return candidate.relativePath === preferred
|
||||||
|
|| candidate.filePath === preferred
|
||||||
|
|| candidate.movePath === preferred;
|
||||||
})
|
})
|
||||||
: files[0];
|
: files[0];
|
||||||
|
|
||||||
@@ -524,58 +527,6 @@ const normalizeToolDiagnostic = (value: unknown): ToolDiagnostic | null => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPrimaryToolPath = (
|
|
||||||
toolName: string,
|
|
||||||
input: Record<string, unknown> | undefined,
|
|
||||||
metadata: Record<string, unknown> | undefined,
|
|
||||||
): string | null => {
|
|
||||||
if (toolName === 'apply_patch') {
|
|
||||||
const files = Array.isArray(metadata?.files) ? metadata.files : [];
|
|
||||||
const first = files.find((entry) => {
|
|
||||||
if (!isRecord(entry)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return entry.type !== 'delete';
|
|
||||||
});
|
|
||||||
if (!isRecord(first)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return typeof first.movePath === 'string'
|
|
||||||
? first.movePath
|
|
||||||
: typeof first.filePath === 'string'
|
|
||||||
? first.filePath
|
|
||||||
: typeof first.relativePath === 'string'
|
|
||||||
? first.relativePath
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolName === 'edit' || toolName === 'multiedit') {
|
|
||||||
const fileDiff = isRecord(metadata?.filediff) ? metadata.filediff : undefined;
|
|
||||||
if (isRecord(fileDiff) && typeof fileDiff.file === 'string') {
|
|
||||||
return fileDiff.file;
|
|
||||||
}
|
|
||||||
return typeof input?.filePath === 'string'
|
|
||||||
? input.filePath
|
|
||||||
: typeof input?.file_path === 'string'
|
|
||||||
? input.file_path
|
|
||||||
: typeof input?.path === 'string'
|
|
||||||
? input.path
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolName === 'write') {
|
|
||||||
return typeof input?.filePath === 'string'
|
|
||||||
? input.filePath
|
|
||||||
: typeof input?.file_path === 'string'
|
|
||||||
? input.file_path
|
|
||||||
: typeof input?.path === 'string'
|
|
||||||
? input.path
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getToolDiagnosticSection = (
|
const getToolDiagnosticSection = (
|
||||||
toolName: string,
|
toolName: string,
|
||||||
input: Record<string, unknown> | undefined,
|
input: Record<string, unknown> | undefined,
|
||||||
@@ -2349,23 +2300,21 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
|||||||
let filePath: unknown;
|
let filePath: unknown;
|
||||||
let targetLine: number | undefined;
|
let targetLine: number | undefined;
|
||||||
let toolDiff: string | undefined;
|
let toolDiff: string | undefined;
|
||||||
if (part.tool === 'edit' || part.tool === 'multiedit') {
|
if (normalizedPartTool === 'edit' || normalizedPartTool === 'multiedit') {
|
||||||
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
||||||
targetLine = getFirstChangedLineFromMetadata(part.tool, metadata);
|
targetLine = getFirstChangedLineFromMetadata(normalizedPartTool, metadata);
|
||||||
if (typeof filePath === 'string') {
|
if (typeof filePath === 'string') {
|
||||||
toolDiff = getPrimaryDiffFromMetadata(part.tool, metadata, filePath);
|
toolDiff = getPrimaryDiffFromMetadata(normalizedPartTool, metadata, filePath);
|
||||||
}
|
}
|
||||||
} else if (part.tool === 'apply_patch') {
|
} else if (normalizedPartTool === 'apply_patch') {
|
||||||
const files = Array.isArray(metadata?.files) ? metadata?.files : [];
|
filePath = getPrimaryToolPath(normalizedPartTool, input, metadata);
|
||||||
const firstFile = files[0] as { relativePath?: string; filePath?: string } | undefined;
|
targetLine = getFirstChangedLineFromMetadata(normalizedPartTool, metadata);
|
||||||
filePath = firstFile?.relativePath || firstFile?.filePath;
|
|
||||||
targetLine = getFirstChangedLineFromMetadata(part.tool, metadata);
|
|
||||||
if (typeof filePath === 'string') {
|
if (typeof filePath === 'string') {
|
||||||
toolDiff = getPrimaryDiffFromMetadata(part.tool, metadata, filePath);
|
toolDiff = getPrimaryDiffFromMetadata(normalizedPartTool, metadata, filePath);
|
||||||
}
|
}
|
||||||
} else if (['write', 'create', 'file_write'].includes(part.tool)) {
|
} else if (['write', 'create', 'file_write'].includes(normalizedPartTool)) {
|
||||||
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
||||||
} else if (part.tool === 'lsp') {
|
} else if (normalizedPartTool === 'lsp') {
|
||||||
filePath = input?.filePath || input?.file_path || input?.path;
|
filePath = input?.filePath || input?.file_path || input?.path;
|
||||||
const line = input?.line;
|
const line = input?.line;
|
||||||
targetLine = typeof line === 'number' && Number.isFinite(line) ? Math.trunc(line) : undefined;
|
targetLine = typeof line === 'number' && Number.isFinite(line) ? Math.trunc(line) : undefined;
|
||||||
@@ -2373,11 +2322,8 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
|||||||
|
|
||||||
if (typeof filePath === 'string') {
|
if (typeof filePath === 'string') {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let absolutePath = filePath;
|
const absolutePath = toAbsoluteFilePath(currentDirectory, filePath);
|
||||||
if (!filePath.startsWith('/')) {
|
if (runtime.runtime.isVSCode && toolDiff && (normalizedPartTool === 'edit' || normalizedPartTool === 'multiedit' || normalizedPartTool === 'apply_patch')) {
|
||||||
absolutePath = currentDirectory.endsWith('/') ? currentDirectory + filePath : currentDirectory + '/' + filePath;
|
|
||||||
}
|
|
||||||
if (runtime.runtime.isVSCode && toolDiff && (part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'apply_patch')) {
|
|
||||||
const label = `${getRelativePath(absolutePath, currentDirectory)} (changes)`;
|
const label = `${getRelativePath(absolutePath, currentDirectory)} (changes)`;
|
||||||
void runtime.editor.openDiff('', absolutePath, label, { line: targetLine, patch: toolDiff });
|
void runtime.editor.openDiff('', absolutePath, label, { line: targetLine, patch: toolDiff });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
import { describe, expect, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import { getDiffPatchEntries, getRenderablePatchInfo } from './toolDiffUtils';
|
import { getDiffPatchEntries, getPrimaryToolPath, getRenderablePatchInfo } from './toolDiffUtils';
|
||||||
|
|
||||||
const identity = (path: string) => path;
|
const identity = (path: string) => path;
|
||||||
|
|
||||||
describe('toolDiffUtils', () => {
|
describe('toolDiffUtils', () => {
|
||||||
|
test('prefers the absolute apply_patch path over its worktree-relative label', () => {
|
||||||
|
expect(getPrimaryToolPath('apply_patch', undefined, {
|
||||||
|
files: [{
|
||||||
|
filePath: '/workspace/project/src/file.ts',
|
||||||
|
relativePath: 'workspace/project/src/file.ts',
|
||||||
|
type: 'update',
|
||||||
|
}],
|
||||||
|
})).toBe('/workspace/project/src/file.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens the move destination and skips deleted apply_patch files', () => {
|
||||||
|
expect(getPrimaryToolPath('apply_patch', undefined, {
|
||||||
|
files: [
|
||||||
|
{ filePath: '/workspace/deleted.ts', relativePath: 'deleted.ts', type: 'delete' },
|
||||||
|
{
|
||||||
|
filePath: '/workspace/old.ts',
|
||||||
|
relativePath: 'new.ts',
|
||||||
|
movePath: '/workspace/new.ts',
|
||||||
|
type: 'move',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})).toBe('/workspace/new.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to the relative apply_patch path for legacy metadata', () => {
|
||||||
|
expect(getPrimaryToolPath('apply_patch', undefined, {
|
||||||
|
files: [{ relativePath: 'src/file.ts', type: 'update' }],
|
||||||
|
})).toBe('src/file.ts');
|
||||||
|
});
|
||||||
|
|
||||||
test('treats raw apply_patch envelopes as text, not visual diffs', () => {
|
test('treats raw apply_patch envelopes as text, not visual diffs', () => {
|
||||||
const entries = getDiffPatchEntries(undefined, [
|
const entries = getDiffPatchEntries(undefined, [
|
||||||
'*** Begin Patch',
|
'*** Begin Patch',
|
||||||
|
|||||||
@@ -140,6 +140,53 @@ export const getPatchText = (value: unknown): string | undefined => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPrimaryToolPath = (
|
||||||
|
toolName: string,
|
||||||
|
input: Record<string, unknown> | undefined,
|
||||||
|
metadata: Record<string, unknown> | undefined,
|
||||||
|
): string | null => {
|
||||||
|
if (toolName === 'apply_patch') {
|
||||||
|
const files = Array.isArray(metadata?.files) ? metadata.files : [];
|
||||||
|
const first = files.find((entry) => isRecord(entry) && entry.type !== 'delete');
|
||||||
|
if (!isRecord(first)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return typeof first.movePath === 'string'
|
||||||
|
? first.movePath
|
||||||
|
: typeof first.filePath === 'string'
|
||||||
|
? first.filePath
|
||||||
|
: typeof first.relativePath === 'string'
|
||||||
|
? first.relativePath
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName === 'edit' || toolName === 'multiedit') {
|
||||||
|
const fileDiff = isRecord(metadata?.filediff) ? metadata.filediff : undefined;
|
||||||
|
if (fileDiff && typeof fileDiff.file === 'string') {
|
||||||
|
return fileDiff.file;
|
||||||
|
}
|
||||||
|
return typeof input?.filePath === 'string'
|
||||||
|
? input.filePath
|
||||||
|
: typeof input?.file_path === 'string'
|
||||||
|
? input.file_path
|
||||||
|
: typeof input?.path === 'string'
|
||||||
|
? input.path
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName === 'write') {
|
||||||
|
return typeof input?.filePath === 'string'
|
||||||
|
? input.filePath
|
||||||
|
: typeof input?.file_path === 'string'
|
||||||
|
? input.file_path
|
||||||
|
: typeof input?.path === 'string'
|
||||||
|
? input.path
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeParsedPath = (path: string | undefined): string => {
|
const normalizeParsedPath = (path: string | undefined): string => {
|
||||||
const trimmed = (path ?? '').trim().replace(/\t.*$/, '');
|
const trimmed = (path ?? '').trim().replace(/\t.*$/, '');
|
||||||
if (!trimmed || trimmed === '/dev/null') {
|
if (!trimmed || trimmed === '/dev/null') {
|
||||||
|
|||||||
Reference in New Issue
Block a user