fix(ui): keep diff refreshes targeted
This commit is contained in:
@@ -373,6 +373,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
);
|
||||
const ensureGitStatus = useGitStore((state) => state.ensureStatus);
|
||||
const fetchGitStatus = useGitStore((state) => state.fetchStatus);
|
||||
const clearGitDiffCache = useGitStore((state) => state.clearDiffCache);
|
||||
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
||||
const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept);
|
||||
const [isNarrowComposer, setIsNarrowComposer] = React.useState(false);
|
||||
@@ -449,9 +450,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
if (!currentDirectory || !runtimeGit) return;
|
||||
return sessionEvents.onGitRefreshHint((hint) => {
|
||||
if (normalizePath(hint.directory) !== normalizePath(currentDirectory)) return;
|
||||
void fetchGitStatus(currentDirectory, runtimeGit);
|
||||
if (hint.paths?.length) {
|
||||
clearGitDiffCache(currentDirectory, hint.paths);
|
||||
}
|
||||
void fetchGitStatus(currentDirectory, runtimeGit, { silent: true });
|
||||
});
|
||||
}, [currentDirectory, runtimeGit, fetchGitStatus]);
|
||||
}, [clearGitDiffCache, currentDirectory, runtimeGit, fetchGitStatus]);
|
||||
|
||||
const handleStartReviewFlow = React.useCallback(async (execution: ReviewFlowExecution) => {
|
||||
if (!currentSessionId) return;
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
extractFirstChangedLineFromDiff,
|
||||
getDiffPatchEntries,
|
||||
getFirstChangedLineFromMetadata,
|
||||
getMutatedToolPaths,
|
||||
getPatchText,
|
||||
getPrimaryDiffFromMetadata,
|
||||
getPrimaryToolPath,
|
||||
@@ -113,7 +114,6 @@ const GIT_REFRESH_MUTATING_TOOLS = new Set([
|
||||
'write',
|
||||
'apply_patch',
|
||||
'patch',
|
||||
'task',
|
||||
]);
|
||||
|
||||
const formatDuration = (start: number, end?: number, now: number = Date.now()) => {
|
||||
@@ -1820,6 +1820,9 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const state = part.state;
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
const input = stateWithData.input;
|
||||
const showToolFileIcons = useUIStore((s) => s.showToolFileIcons);
|
||||
const currentDirectory = useEffectiveDirectory() ?? '';
|
||||
|
||||
@@ -1828,18 +1831,19 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
||||
|
||||
const status = state?.status as string | undefined;
|
||||
const isFinalized = status === 'completed' || status === 'error' || status === 'aborted' || status === 'failed' || status === 'timeout' || status === 'cancelled';
|
||||
const isSuccessfullyFinalized = status === 'completed';
|
||||
const isError = status === 'error' || status === 'failed';
|
||||
|
||||
const [activeLatched, setActiveLatched] = React.useState<boolean>(!isFinalized);
|
||||
const previousPartIdRef = React.useRef<string | undefined>(part.id);
|
||||
const lastGitRefreshSignatureRef = React.useRef<string>('');
|
||||
const observedActiveGitToolRef = React.useRef(!isFinalized);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (previousPartIdRef.current === part.id) {
|
||||
return;
|
||||
}
|
||||
previousPartIdRef.current = part.id;
|
||||
lastGitRefreshSignatureRef.current = '';
|
||||
observedActiveGitToolRef.current = !isFinalized;
|
||||
// Reset latch only when tool identity changes.
|
||||
setActiveLatched(!isFinalized);
|
||||
}, [isFinalized, part.id]);
|
||||
@@ -1851,20 +1855,34 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
||||
}, [isFinalized]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isFinalized || isError || !currentDirectory) {
|
||||
return;
|
||||
}
|
||||
if (!GIT_REFRESH_MUTATING_TOOLS.has(normalizedPartTool)) {
|
||||
if (!isFinalized) {
|
||||
observedActiveGitToolRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = `${part.id}:${status ?? 'unknown'}`;
|
||||
if (lastGitRefreshSignatureRef.current === signature) {
|
||||
// Historical completed tools can remount when the timeline changes.
|
||||
// Refresh only for a tool whose active state this instance observed.
|
||||
const finalizedAfterObservedActive = observedActiveGitToolRef.current;
|
||||
if (!finalizedAfterObservedActive) {
|
||||
return;
|
||||
}
|
||||
lastGitRefreshSignatureRef.current = signature;
|
||||
sessionEvents.requestGitRefresh({ directory: currentDirectory });
|
||||
}, [currentDirectory, isError, isFinalized, normalizedPartTool, part.id, status]);
|
||||
|
||||
if (!isSuccessfullyFinalized || !GIT_REFRESH_MUTATING_TOOLS.has(normalizedPartTool)) {
|
||||
observedActiveGitToolRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (!currentDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
observedActiveGitToolRef.current = false;
|
||||
const paths = getMutatedToolPaths(normalizedPartTool, input, metadata)
|
||||
.map((path) => getRelativePath(path, currentDirectory));
|
||||
sessionEvents.requestGitRefresh({
|
||||
directory: currentDirectory,
|
||||
...(paths.length > 0 ? { paths } : {}),
|
||||
});
|
||||
}, [currentDirectory, input, isFinalized, isSuccessfullyFinalized, metadata, normalizedPartTool]);
|
||||
|
||||
const shouldNotifyStructuralChange = isFinalized || isTaskTool;
|
||||
|
||||
@@ -1890,10 +1908,7 @@ const ToolPartContent: React.FC<ToolPartProps> = ({
|
||||
}
|
||||
}, [isExpanded, isTaskTool, shouldNotifyStructuralChange]);
|
||||
|
||||
const stateWithData = state as ToolStateWithMetadata;
|
||||
const metadata = stateWithData.metadata;
|
||||
const partMetadata = (part as unknown as { metadata?: unknown }).metadata;
|
||||
const input = stateWithData.input;
|
||||
const time = stateWithData.time;
|
||||
|
||||
const [pinnedTime, setPinnedTime] = React.useState<{ start?: number; end?: number }>(() => ({
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getApplyPatchFilePath,
|
||||
getDiffPatchEntries,
|
||||
getFirstChangedLineFromMetadata,
|
||||
getMutatedToolPaths,
|
||||
getPrimaryDiffFromMetadata,
|
||||
getPrimaryToolPath,
|
||||
getRenderablePatchInfo,
|
||||
@@ -54,6 +55,28 @@ describe('toolDiffUtils', () => {
|
||||
})).toBe('/workspace/project/src/second.ts');
|
||||
});
|
||||
|
||||
test('lists every apply_patch mutation path, including both sides of a move', () => {
|
||||
expect(getMutatedToolPaths('apply_patch', undefined, {
|
||||
files: [
|
||||
{ filePath: '/workspace/project/src/deleted.ts', type: 'delete' },
|
||||
{
|
||||
filePath: '/workspace/project/src/old.ts',
|
||||
movePath: '/workspace/project/src/new.ts',
|
||||
type: 'move',
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
'/workspace/project/src/deleted.ts',
|
||||
'/workspace/project/src/new.ts',
|
||||
'/workspace/project/src/old.ts',
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not invent paths for bash or task tools', () => {
|
||||
expect(getMutatedToolPaths('bash', { command: 'date' }, undefined)).toEqual([]);
|
||||
expect(getMutatedToolPaths('task', { description: 'inspect' }, undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test('selects the move patch and line from the same non-deleted file', () => {
|
||||
const deletedPatch = '@@ -3 +3 @@\n-old\n+deleted';
|
||||
const movedPatch = '@@ -42 +42 @@\n-before\n+after';
|
||||
|
||||
@@ -200,6 +200,29 @@ export const getPrimaryToolPath = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getMutatedToolPaths = (
|
||||
toolName: string,
|
||||
input: Record<string, unknown> | undefined,
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
): string[] => {
|
||||
if (toolName === 'apply_patch') {
|
||||
const files = Array.isArray(metadata?.files) ? metadata.files : [];
|
||||
const paths = new Set<string>();
|
||||
for (const file of files) {
|
||||
if (!isRecord(file)) continue;
|
||||
const filePath = getApplyPatchFilePath(file);
|
||||
if (filePath) paths.add(filePath);
|
||||
if (file.type === 'move' && typeof file.filePath === 'string') {
|
||||
paths.add(file.filePath);
|
||||
}
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
const primaryPath = getPrimaryToolPath(toolName, input, metadata);
|
||||
return primaryPath ? [primaryPath] : [];
|
||||
};
|
||||
|
||||
const supportsDiffMetadata = (toolName: string): boolean => (
|
||||
toolName === 'edit' || toolName === 'multiedit' || toolName === 'apply_patch'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user