From 209ffc16e455d5e4b537c0c6070c79d2877c9c05 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 26 Apr 2026 16:58:07 +0300 Subject: [PATCH] fix: handle multi-file tool diffs safely Split multi-file patches before diff rendering Fixed hook dependency lint warnings --- packages/ui/src/components/chat/ChatInput.tsx | 12 ++-- .../components/chat/message/MessageBody.tsx | 4 +- .../chat/message/TextSelectionMenu.tsx | 2 +- .../chat/message/ToolOutputDialog.tsx | 2 +- .../chat/message/parts/ToolPart.tsx | 69 ++++++++++++++++--- .../desktop/DesktopHostSwitcher.tsx | 2 +- packages/ui/src/components/layout/Header.tsx | 2 +- .../layout/ProjectActionsButton.tsx | 4 +- .../components/sections/agents/AgentsPage.tsx | 2 +- .../sections/openchamber/VoiceSettings.tsx | 2 +- .../sections/projects/ProjectsPage.tsx | 1 + .../sections/providers/ProvidersPage.tsx | 8 +-- .../remote-instances/RemoteInstancesPage.tsx | 2 +- .../session/GitHubIssuePickerDialog.tsx | 2 +- .../session/GitHubPrPickerDialog.tsx | 2 +- .../components/session/NewWorktreeDialog.tsx | 2 + .../ui/src/components/views/FilesView.tsx | 2 +- packages/ui/src/components/views/GitView.tsx | 18 ++--- .../components/views/git/ChangesSection.tsx | 1 + .../components/views/git/ConflictDialog.tsx | 2 +- .../views/git/IntegrateCommitsSection.tsx | 8 +-- .../views/git/PullRequestSection.tsx | 24 +++---- 22 files changed, 116 insertions(+), 57 deletions(-) diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 80de5dd6..a0f4683d 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -2341,7 +2341,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.clipboardAttachFailed')); } } - }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]); + }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection, t]); const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => { @@ -2650,7 +2650,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo setPendingInputText(mentions.join(' '), 'append-inline'); toast.success(t('chat.chatInput.toast.addedFileMentions', { count: mentions.length })); - }, [normalizeDroppedPath, setPendingInputText, toProjectRelativeMentionPath]); + }, [normalizeDroppedPath, setPendingInputText, t, toProjectRelativeMentionPath]); const handleDragEnter = (e: React.DragEvent) => { if (!hasDraggedFiles(e.dataTransfer)) { @@ -2897,7 +2897,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo cancelled = true; if (unlisten) unlisten(); }; - }, [addAttachedFile, normalizeDroppedPath]); + }, [addAttachedFile, normalizeDroppedPath, t]); const fileInputRef = React.useRef(null); @@ -2912,7 +2912,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.attachFileFailed')); } } - }, [addAttachedFile]); + }, [addAttachedFile, t]); const handleVSCodePickFiles = React.useCallback(async () => { try { @@ -2956,7 +2956,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo console.error('VS Code file pick failed', error); toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.vscodePickFailed')); } - }, [attachFiles]); + }, [attachFiles, t]); const handlePickLocalFiles = React.useCallback(() => { if (isVSCodeRuntime()) { @@ -3268,7 +3268,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo setSessionAutoAccept(permissionScopeSessionId, nextEnabled).catch(() => { toast.error(t('chat.chatInput.toast.togglePermissionAutoAcceptFailed')); }); - }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept]); + }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept, t]); React.useEffect(() => { const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index a24aab78..23d37be2 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -1010,7 +1010,7 @@ const AssistantMessageBody = React.memo(({ setIsSavingPlan(false); } }, - [assistantPlanText, currentProjectRef] + [assistantPlanText, currentProjectRef, t] ); const [isSharing, setIsSharing] = React.useState(false); @@ -1123,7 +1123,7 @@ const AssistantMessageBody = React.memo(({ setIsSharing(false); } }, - [messageId, isSharing] + [messageId, isSharing, t] ); React.useEffect(() => { diff --git a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx index ab344e98..ff05e0c3 100644 --- a/packages/ui/src/components/chat/message/TextSelectionMenu.tsx +++ b/packages/ui/src/components/chat/message/TextSelectionMenu.tsx @@ -534,7 +534,7 @@ export const TextSelectionMenu: React.FC = ({ containerR } finally { setIsAddingToNotes(false); } - }, [currentProjectRef, hideMenu, selectedText]); + }, [currentProjectRef, hideMenu, selectedText, t]); if (!position.show) return null; diff --git a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx index 793f0877..5041c9aa 100644 --- a/packages/ui/src/components/chat/message/ToolOutputDialog.tsx +++ b/packages/ui/src/components/chat/message/ToolOutputDialog.tsx @@ -782,7 +782,7 @@ const MermaidPreviewDialog: React.FC<{ setStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('chat.toolOutputDialog.mermaid.loadFailed')); }); - }, [decodeDataUrl, normalizeFilePath, popup.mermaid]); + }, [decodeDataUrl, normalizeFilePath, popup.mermaid, t]); React.useEffect(() => { if (!popup.open || !popup.mermaid) { diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index 4d1530f0..2db7172a 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -1243,6 +1243,56 @@ type DiffPatchEntry = { patch: string; }; +const hasUnifiedDiffHunk = (patch: string): boolean => /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@/m.test(patch); + +const getUnifiedDiffPath = (patch: string, fallbackTitle: string): string => { + const plusHeader = patch.match(/^\+\+\+\s+(?:[ab]\/(.+)|(.+))$/m); + const rawPath = plusHeader?.[1] ?? plusHeader?.[2]; + if (!rawPath || rawPath === '/dev/null') { + return fallbackTitle; + } + return rawPath; +}; + +const splitUnifiedDiffPatch = (patch: string): DiffPatchEntry[] => { + const normalized = patch.replace(/\r\n/g, '\n').trim(); + if (!normalized) { + return []; + } + + const lines = normalized.split('\n'); + const starts: number[] = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] ?? ''; + const nextLine = lines[index + 1] ?? ''; + const isUnifiedFileHeader = /^---\s+(?:[ab]\/|\/dev\/null|\/)/.test(line) + && /^\+\+\+\s+(?:[ab]\/|\/dev\/null|\/)/.test(nextLine); + if (line.startsWith('diff --git ') || line.startsWith('Index: ') || isUnifiedFileHeader) { + starts.push(index); + } + } + + const chunks = starts.length > 0 + ? starts.map((start, index) => lines.slice(start, starts[index + 1] ?? lines.length).join('\n').trim()) + : [normalized]; + + return chunks + .map((chunk, index) => { + if (!hasUnifiedDiffHunk(chunk)) { + return null; + } + + const title = getUnifiedDiffPath(chunk, `Diff ${index + 1}`); + return { + id: `${title}-${index}`, + title, + patch: chunk, + } satisfies DiffPatchEntry; + }) + .filter((entry): entry is DiffPatchEntry => entry !== null); +}; + const renderPathLikeGitChanges = (path: string, grow = true) => { const lastSlash = path.lastIndexOf('/'); if (lastSlash === -1) { @@ -1338,7 +1388,7 @@ const getDiffPatchEntries = ( const record = file as { relativePath?: unknown; filePath?: unknown; patch?: unknown; diff?: unknown }; const patch = getPatchText(record.patch) ?? getPatchText(record.diff) ?? ''; - if (!patch) { + if (!patch || !hasUnifiedDiffHunk(patch)) { return null; } @@ -1364,13 +1414,16 @@ const getDiffPatchEntries = ( return entries; } - return [ - { - id: 'diff-0', - title: 'Diff', - patch: fallbackDiff, - }, - ]; + const splitEntries = splitUnifiedDiffPatch(fallbackDiff).map((entry) => ({ + ...entry, + title: getRelativePath(entry.title, currentDirectory), + })); + + if (splitEntries.length > 0) { + return splitEntries; + } + + return []; }; const DiffPreview: React.FC = React.memo(({ diff, pierreTheme, pierreThemeType, diffViewMode }) => { diff --git a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx index 24901bef..34f57a14 100644 --- a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx +++ b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx @@ -1307,7 +1307,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost cancelled = true; window.clearInterval(interval); }; - }, [connectDefaultSshInstance]); + }, [connectDefaultSshInstance, t]); if (!isDesktopShell()) { return null; diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 057c5383..2aff714f 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -930,7 +930,7 @@ export const Header: React.FC = ({ } return groups; - }, [dropdownProviderIds, quotaResults, selectedModels]); + }, [dropdownProviderIds, quotaResults, selectedModels, t]); const hasRateLimits = rateLimitGroups.length > 0; React.useEffect(() => { void loadQuotaSettings(); diff --git a/packages/ui/src/components/layout/ProjectActionsButton.tsx b/packages/ui/src/components/layout/ProjectActionsButton.tsx index eb956a55..1f1b3fcb 100644 --- a/packages/ui/src/components/layout/ProjectActionsButton.tsx +++ b/packages/ui/src/components/layout/ProjectActionsButton.tsx @@ -370,7 +370,7 @@ export const ProjectActionsButton = ({ } } - }, [actions, openExternal, runningByKey, terminalSessions]); + }, [actions, openExternal, runningByKey, t, terminalSessions]); const normalizedDirectory = React.useMemo(() => { return normalizeProjectActionDirectory(directory || stableProjectRef?.path || ''); @@ -424,6 +424,7 @@ export const ProjectActionsButton = ({ setActiveTab, setBottomTerminalOpen, setTabLabel, + t, ]); const runAction = React.useCallback(async (action: OpenChamberProjectAction) => { @@ -529,6 +530,7 @@ export const ProjectActionsButton = ({ runtime.isVSCode, setConnecting, setTabSessionId, + t, terminal, ]); diff --git a/packages/ui/src/components/sections/agents/AgentsPage.tsx b/packages/ui/src/components/sections/agents/AgentsPage.tsx index 634e2d69..878a4765 100644 --- a/packages/ui/src/components/sections/agents/AgentsPage.tsx +++ b/packages/ui/src/components/sections/agents/AgentsPage.tsx @@ -422,7 +422,7 @@ export const AgentsPage: React.FC = () => { } setPendingRuleName(''); setPendingRulePattern('*'); - }, [globalPermission, pendingRuleName, pendingRulePattern, removeRule, setGlobalPermissionAndPrune, upsertRule]); + }, [globalPermission, pendingRuleName, pendingRulePattern, removeRule, setGlobalPermissionAndPrune, t, upsertRule]); const formatPermissionLabel = React.useCallback((permissionName: string): string => { if (permissionName === '*') return t('settings.agents.page.permissions.defaultLabel'); diff --git a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx index e9330dd4..8b5451fa 100644 --- a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx @@ -181,7 +181,7 @@ export const VoiceSettings: React.FC = () => { window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); - }, [browserVoice, browserVoices, speechRate, speechPitch, speechVolume, isBrowserPreviewPlaying]); + }, [browserVoice, browserVoices, speechRate, speechPitch, speechVolume, isBrowserPreviewPlaying, t]); useEffect(() => { return () => { diff --git a/packages/ui/src/components/sections/projects/ProjectsPage.tsx b/packages/ui/src/components/sections/projects/ProjectsPage.tsx index 774fb162..c8d9584c 100644 --- a/packages/ui/src/components/sections/projects/ProjectsPage.tsx +++ b/packages/ui/src/components/sections/projects/ProjectsPage.tsx @@ -150,6 +150,7 @@ export const ProjectsPage: React.FC = () => { uploadProjectIcon, removeProjectIcon, selectedProject, + t, updateProjectMeta, ]); diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.tsx b/packages/ui/src/components/sections/providers/ProvidersPage.tsx index 6a167e50..c446aac3 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersPage.tsx @@ -207,7 +207,7 @@ export const ProvidersPage: React.FC = () => { return () => { isMounted = false; }; - }, []); + }, [t]); React.useEffect(() => { let isMounted = true; @@ -244,7 +244,7 @@ export const ProvidersPage: React.FC = () => { return () => { isMounted = false; }; - }, []); + }, [t]); const connectedProviderIds = React.useMemo( () => new Set(providers.map((provider) => provider.id)), @@ -280,7 +280,7 @@ export const ProvidersPage: React.FC = () => { } setShowAuthPanel(false); - }, [selectedProviderId]); + }, [selectedProviderId, t]); React.useEffect(() => { if (!selectedProviderId || selectedProviderId === ADD_PROVIDER_ID) { @@ -320,7 +320,7 @@ export const ProvidersPage: React.FC = () => { return () => { cancelled = true; }; - }, [selectedProviderId]); + }, [selectedProviderId, t]); const selectedProvider = providers.find((provider) => provider.id === selectedProviderId); const selectedSources = selectedProviderId ? providerSources[selectedProviderId] : undefined; diff --git a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx index d0660358..a8c8e2d7 100644 --- a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx +++ b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx @@ -443,7 +443,7 @@ export const RemoteInstancesPage: React.FC = () => { return false; } }, - [createFromCommand, setSelectedId], + [createFromCommand, setSelectedId, t], ); const closePatternDialog = React.useCallback(() => { diff --git a/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx b/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx index 319f92aa..7f541c00 100644 --- a/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx +++ b/packages/ui/src/components/session/GitHubIssuePickerDialog.tsx @@ -136,7 +136,7 @@ export function GitHubIssuePickerDialog({ } finally { setIsLoading(false); } - }, [github, githubAuthChecked, githubAuthStatus, projectDirectory]); + }, [github, githubAuthChecked, githubAuthStatus, projectDirectory, t]); const loadMore = React.useCallback(async () => { if (!projectDirectory) return; diff --git a/packages/ui/src/components/session/GitHubPrPickerDialog.tsx b/packages/ui/src/components/session/GitHubPrPickerDialog.tsx index 80c7237f..d9badd1d 100644 --- a/packages/ui/src/components/session/GitHubPrPickerDialog.tsx +++ b/packages/ui/src/components/session/GitHubPrPickerDialog.tsx @@ -126,7 +126,7 @@ export function GitHubPrPickerDialog({ } finally { setIsLoading(false); } - }, [github, githubAuthChecked, githubAuthStatus, projectDirectory]); + }, [github, githubAuthChecked, githubAuthStatus, projectDirectory, t]); const loadMore = React.useCallback(async () => { if (!projectDirectory) return; diff --git a/packages/ui/src/components/session/NewWorktreeDialog.tsx b/packages/ui/src/components/session/NewWorktreeDialog.tsx index fdffa3d5..f8926629 100644 --- a/packages/ui/src/components/session/NewWorktreeDialog.tsx +++ b/packages/ui/src/components/session/NewWorktreeDialog.tsx @@ -618,6 +618,7 @@ export function NewWorktreeDialog({ resolveDefaultAgentName, resolveDefaultModelSelection, resolveDefaultVariant, + t, ]); // Get current state based on mode @@ -788,6 +789,7 @@ export function NewWorktreeDialog({ validation.touched, validationAbortController, isCreating, + t, ]); // Extract branch name for dependency array diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 8fab38ad..4cd8d502 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -2329,7 +2329,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return () => { cancelled = true; }; - }, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path]); + }, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, t]); const renderDialogs = () => ( !open && setActiveDialog(null)}> diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index db371e77..c70b85a7 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -785,7 +785,7 @@ export const GitView: React.FC = () => { } } }, - [currentDirectory, git, fetchStatus, fetchBranches] + [currentDirectory, git, fetchStatus, fetchBranches, t] ); const refreshLog = React.useCallback(async () => { @@ -972,7 +972,7 @@ export const GitView: React.FC = () => { } finally { setRemovingRemoteName(null); } - }, [currentDirectory, git, refreshRemotes, refreshStatusAndBranches]); + }, [currentDirectory, git, refreshRemotes, refreshStatusAndBranches, t]); const handleCommit = async (options: { pushAfter?: boolean } = {}) => { if (!currentDirectory) return; @@ -1066,7 +1066,7 @@ export const GitView: React.FC = () => { } finally { setIsGeneratingMessage(false); } - }, [currentDirectory, selectedPaths, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom]); + }, [currentDirectory, selectedPaths, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom, t]); const formatBlockingReason = (reason: ReturnType[number]): string => { if (reason.reason === 'attention') { @@ -1517,7 +1517,7 @@ export const GitView: React.FC = () => { }); } }, - [currentDirectory, refreshStatusAndBranches, git] + [currentDirectory, refreshStatusAndBranches, git, t] ); const handleRevertAll = React.useCallback( @@ -1575,7 +1575,7 @@ export const GitView: React.FC = () => { setIsRevertingAll(false); } }, - [currentDirectory, git, isRevertingAll, refreshStatusAndBranches] + [currentDirectory, git, isRevertingAll, refreshStatusAndBranches, t] ); const handleInsertHighlights = React.useCallback((sourceHighlights: string[]) => { @@ -1787,7 +1787,7 @@ export const GitView: React.FC = () => { const message = err instanceof Error ? err.message : `Failed to abort ${conflictOperation}`; toast.error(message); } - }, [currentDirectory, git, conflictOperation, refreshStatusAndBranches, refreshLog, clearConflictState]); + }, [currentDirectory, git, conflictOperation, refreshStatusAndBranches, refreshLog, clearConflictState, t]); // Check if there are unresolved conflicts (files with 'U' status) const hasUnresolvedConflicts = React.useMemo(() => { @@ -1839,7 +1839,7 @@ export const GitView: React.FC = () => { const message = err instanceof Error ? err.message : t('gitView.toast.continueOperationFailed'); toast.error(message); } - }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, persistConflictState, clearConflictState]); + }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, persistConflictState, clearConflictState, t]); const handleAbortOperation = React.useCallback(async () => { if (!currentDirectory) return; @@ -1860,7 +1860,7 @@ export const GitView: React.FC = () => { const message = err instanceof Error ? err.message : t('gitView.toast.abortOperationFailed'); toast.error(message); } - }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, clearConflictState]); + }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, clearConflictState, t]); const handleResolveWithAIFromBanner = React.useCallback(() => { if (!currentDirectory) return; @@ -1958,7 +1958,7 @@ export const GitView: React.FC = () => { throw err; } }, - [currentDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog] + [currentDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog, t] ); if (!currentDirectory) { diff --git a/packages/ui/src/components/views/git/ChangesSection.tsx b/packages/ui/src/components/views/git/ChangesSection.tsx index 6f3df735..60b79a5a 100644 --- a/packages/ui/src/components/views/git/ChangesSection.tsx +++ b/packages/ui/src/components/views/git/ChangesSection.tsx @@ -429,6 +429,7 @@ export const ChangesSection: React.FC = ({ revertingPaths, rowPaddingClassName, selectedPaths, + t, toggleDirectoryExpanded, toggleDirectorySelection, ]); diff --git a/packages/ui/src/components/views/git/ConflictDialog.tsx b/packages/ui/src/components/views/git/ConflictDialog.tsx index f12f9c62..28d1168d 100644 --- a/packages/ui/src/components/views/git/ConflictDialog.tsx +++ b/packages/ui/src/components/views/git/ConflictDialog.tsx @@ -66,7 +66,7 @@ export const ConflictDialog: React.FC = ({ .finally(() => { setIsLoading(false); }); - }, [open, directory]); + }, [open, directory, t]); const buildConflictContext = React.useCallback(async (): Promise<{ visibleText: string; diff --git a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx index 652d2dac..e8be153c 100644 --- a/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx +++ b/packages/ui/src/components/views/git/IntegrateCommitsSection.tsx @@ -252,7 +252,7 @@ export const IntegrateCommitsSection: React.FC<{ { text: context.payloadText, synthetic: true }, ]); setActiveMainTab('chat'); - }, [currentSessionId, setActiveMainTab, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts]); + }, [currentSessionId, setActiveMainTab, buildConflictContext, openNewSessionDraft, setPendingInputText, setPendingSyntheticParts, t]); const handleMove = React.useCallback(async () => { if (ui.kind !== 'ready') return; @@ -288,7 +288,7 @@ export const IntegrateCommitsSection: React.FC<{ if (next) setUi({ kind: 'ready', plan: next }); else setUi({ kind: 'idle' }); } - }, [ui, onRefresh, repoRoot, sourceBranch, targetBranch, conflictStorageKey]); + }, [ui, onRefresh, repoRoot, sourceBranch, targetBranch, conflictStorageKey, t]); const handleAbort = React.useCallback(async () => { if (ui.kind !== 'conflict') return; @@ -303,7 +303,7 @@ export const IntegrateCommitsSection: React.FC<{ if (next) setUi({ kind: 'ready', plan: next }); else setUi({ kind: 'idle' }); } - }, [ui, repoRoot, sourceBranch, targetBranch, conflictStorageKey]); + }, [ui, repoRoot, sourceBranch, targetBranch, conflictStorageKey, t]); const handleContinue = React.useCallback(async () => { if (ui.kind !== 'conflict') return; @@ -330,7 +330,7 @@ export const IntegrateCommitsSection: React.FC<{ const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.integrate.cherryPickContinueFailedToast'), { description: message }); } - }, [ui, repoRoot, sourceBranch, targetBranch, onRefresh, conflictStorageKey]); + }, [ui, repoRoot, sourceBranch, targetBranch, onRefresh, conflictStorageKey, t]); if (!repoRoot || !sourceBranch) { return null; diff --git a/packages/ui/src/components/views/git/PullRequestSection.tsx b/packages/ui/src/components/views/git/PullRequestSection.tsx index 5e4dcb38..01e4038e 100644 --- a/packages/ui/src/components/views/git/PullRequestSection.tsx +++ b/packages/ui/src/components/views/git/PullRequestSection.tsx @@ -538,7 +538,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsLoadingCheckDetails(false); } - }, [directory, github, pr]); + }, [directory, github, pr, t]); const openCommentsDialog = React.useCallback(async () => { if (!github?.prContext) { @@ -561,7 +561,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsLoadingCommentsDetails(false); } - }, [directory, github, pr]); + }, [directory, github, pr, t]); const formatTimestamp = React.useCallback((value?: string) => { if (!value) return ''; @@ -650,7 +650,7 @@ export const PullRequestSection: React.FC<{ currentAgentName: currentAgentName ?? null, currentVariant: currentVariant ?? null, }; - }, [currentSessionId]); + }, [currentSessionId, t]); const dispatchSyntheticPrompt = React.useCallback(( target: ChatDispatchTarget, @@ -674,7 +674,7 @@ export const PullRequestSection: React.FC<{ const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.pr.toast.sendMessageFailed'), { description: message }); }); - }, []); + }, [t]); const renderCheckRunSummary = React.useCallback((run: GitHubCheckRun) => { const status = run.status || 'unknown'; @@ -804,7 +804,7 @@ export const PullRequestSection: React.FC<{ ) : null} ); - }, [expandedCheckStepKeys, formatTimestamp]); + }, [expandedCheckStepKeys, formatTimestamp, t]); const sendFailedChecksToChat = React.useCallback(async () => { setActiveMainTab('chat'); @@ -860,7 +860,7 @@ export const PullRequestSection: React.FC<{ const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.pr.toast.loadChecksFailed'), { description: message }); } - }, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]); + }, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab, t]); const sendCommentsToChat = React.useCallback(async () => { setActiveMainTab('chat'); @@ -899,7 +899,7 @@ export const PullRequestSection: React.FC<{ const message = e instanceof Error ? e.message : String(e); toast.error(t('gitView.pr.toast.loadPrCommentsFailed'), { description: message }); } - }, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab]); + }, [directory, dispatchSyntheticPrompt, github, pr, resolveChatDispatchTarget, setActiveMainTab, t]); const sendSingleCommentToChat = React.useCallback(async (comment: TimelineCommentItem) => { setCommentsDialogOpen(false); @@ -1142,7 +1142,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsGenerating(false); } - }, [additionalContext, branch, directory, isGenerating, onGeneratedDescription, targetBaseBranch]); + }, [additionalContext, branch, directory, isGenerating, onGeneratedDescription, targetBaseBranch, t]); const createPr = React.useCallback(async () => { if (!github?.prCreate) { @@ -1188,7 +1188,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsCreating(false); } - }, [body, branch, directory, draft, github, prStatusKey, refresh, scheduleActionRefresh, selectedRemote, targetBaseBranch, title, updatePrStatus]); + }, [body, branch, directory, draft, github, prStatusKey, refresh, scheduleActionRefresh, selectedRemote, targetBaseBranch, title, updatePrStatus, t]); const mergePr = React.useCallback(async (pr: GitHubPullRequest) => { if (!github?.prMerge) { @@ -1214,7 +1214,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsMerging(false); } - }, [directory, github, mergeMethod, refresh, scheduleActionRefresh]); + }, [directory, github, mergeMethod, refresh, scheduleActionRefresh, t]); const markReady = React.useCallback(async (pr: GitHubPullRequest) => { if (!github?.prReady) { @@ -1236,7 +1236,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsMarkingReady(false); } - }, [directory, github, refresh, scheduleActionRefresh]); + }, [directory, github, refresh, scheduleActionRefresh, t]); const updatePr = React.useCallback(async (pr: GitHubPullRequest) => { if (!github?.prUpdate) { @@ -1277,7 +1277,7 @@ export const PullRequestSection: React.FC<{ } finally { setIsUpdating(false); } - }, [directory, editBody, editTitle, github, prStatusKey, refresh, scheduleActionRefresh, updatePrStatus]); + }, [directory, editBody, editTitle, github, prStatusKey, refresh, scheduleActionRefresh, updatePrStatus, t]); if (!canShow) { return null;