mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
fix(source-control): only dismiss entry failures from the owning worktre
Track which worktree owns the shared entry-failure toast slot. When a mutation completes, only dismiss the slot if the completing worktree is the one that raised the failure — a slow retry in one worktree should not erase a failure another worktree has since raised into the slot.
This commit is contained in:
+16
-1
@@ -123,7 +123,22 @@ describe('showSourceControlEntryFailureToast', () => {
|
||||
})
|
||||
|
||||
it('clears the shared slot when an attempt finally lands', () => {
|
||||
dismissSourceControlEntryFailureToast()
|
||||
show()
|
||||
dismissSourceControlEntryFailureToast('wt-1')
|
||||
expect(toastDismiss).toHaveBeenCalledWith('source-control-entry-mutation')
|
||||
})
|
||||
|
||||
it('leaves a failure another worktree raised into the slot alone', () => {
|
||||
// Why: a retry still in flight in the worktree the user left must not erase the failure the
|
||||
// worktree they switched to has since raised into the shared slot.
|
||||
show({ worktreeId: 'wt-1' })
|
||||
storeState.activeWorktreeId = 'wt-2'
|
||||
show({ worktreeId: 'wt-2', worktreeName: 'feature-b' })
|
||||
|
||||
dismissSourceControlEntryFailureToast('wt-1')
|
||||
expect(toastDismiss).not.toHaveBeenCalled()
|
||||
|
||||
dismissSourceControlEntryFailureToast('wt-2')
|
||||
expect(toastDismiss).toHaveBeenCalledWith('source-control-entry-mutation')
|
||||
})
|
||||
|
||||
|
||||
+15
-3
@@ -7,8 +7,19 @@ export type SourceControlEntryOperation = 'stage' | 'unstage' | 'discard'
|
||||
|
||||
const ENTRY_FAILURE_TOAST_ID = 'source-control-entry-mutation'
|
||||
|
||||
/** Clears the shared entry-failure slot once an attempt — or its retry — lands. */
|
||||
export function dismissSourceControlEntryFailureToast(): void {
|
||||
// Why: worktreeId is nullable, so an occupancy wrapper distinguishes an empty slot from a null-owned one.
|
||||
let entryFailureSlotOwner: { worktreeId: string | null } | null = null
|
||||
|
||||
/**
|
||||
* Clears the shared entry-failure slot once an attempt — or its retry — lands, but only when the
|
||||
* completing attempt is the one that filled it: a slow retry in a worktree the user has left must
|
||||
* not erase a failure the worktree they moved to has since raised into the same slot.
|
||||
*/
|
||||
export function dismissSourceControlEntryFailureToast(worktreeId: string | null): void {
|
||||
if (!entryFailureSlotOwner || entryFailureSlotOwner.worktreeId !== worktreeId) {
|
||||
return
|
||||
}
|
||||
entryFailureSlotOwner = null
|
||||
toast.dismiss(ENTRY_FAILURE_TOAST_ID)
|
||||
}
|
||||
|
||||
@@ -76,6 +87,7 @@ export function showSourceControlEntryFailureToast({
|
||||
const isActiveWorktree = useAppStore.getState().activeWorktreeId === worktreeId
|
||||
const title = entryFailureTitle(operation, filePath, deleteShaped)
|
||||
const offerRetry = Boolean(onRetry) && isActiveWorktree
|
||||
entryFailureSlotOwner = { worktreeId }
|
||||
toast.error(
|
||||
isActiveWorktree || !worktreeName
|
||||
? title
|
||||
@@ -99,7 +111,7 @@ export function showSourceControlEntryFailureToast({
|
||||
// caller owns this slot instead: it dismisses on success and re-raises on failure.
|
||||
event.preventDefault()
|
||||
if (useAppStore.getState().activeWorktreeId !== worktreeId) {
|
||||
dismissSourceControlEntryFailureToast()
|
||||
dismissSourceControlEntryFailureToast(worktreeId)
|
||||
return
|
||||
}
|
||||
onRetry()
|
||||
|
||||
+6
-1
@@ -155,12 +155,17 @@ describe('source-control entry mutation failures', () => {
|
||||
})
|
||||
|
||||
it('leaves a successful stage silent, and clears a stale failure it supersedes', async () => {
|
||||
mocks.stagePath.mockResolvedValue(undefined)
|
||||
mocks.stagePath.mockRejectedValueOnce(new Error('index.lock exists'))
|
||||
mocks.stagePath.mockResolvedValueOnce(undefined)
|
||||
const { result } = renderMutations()
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleStage('src/app.ts')
|
||||
})
|
||||
mocks.toastError.mockClear()
|
||||
await act(async () => {
|
||||
await result.current.handleStage('src/other.ts')
|
||||
})
|
||||
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
expect(mocks.toastDismiss).toHaveBeenCalledWith('source-control-entry-mutation')
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export function useSourceControlDiscardConfirmation({
|
||||
})
|
||||
return
|
||||
}
|
||||
dismissSourceControlEntryFailureToast()
|
||||
dismissSourceControlEntryFailureToast(activeWorktreeId)
|
||||
await refreshEntryMutationStatus(refreshActiveGitStatusAfterMutation)
|
||||
},
|
||||
[activeWorktreeId, discardSingle, refreshActiveGitStatusAfterMutation, worktreePath]
|
||||
|
||||
+3
-2
@@ -66,8 +66,9 @@ export function useSourceControlEntryMutations({
|
||||
})
|
||||
return
|
||||
}
|
||||
// Why: the mutation landed, so clear any failure this attempt (or an earlier one) left in the slot.
|
||||
dismissSourceControlEntryFailureToast()
|
||||
// Why: the mutation landed, so clear any failure this worktree's attempts left in the slot —
|
||||
// a failure another worktree raised meanwhile is not ours to dismiss.
|
||||
dismissSourceControlEntryFailureToast(activeWorktreeId)
|
||||
await refreshEntryMutationStatus(refreshActiveGitStatusAfterMutation)
|
||||
},
|
||||
[activeRepoSettings, worktreePath, activeWorktreeId, refreshActiveGitStatusAfterMutation]
|
||||
|
||||
Reference in New Issue
Block a user