diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx
new file mode 100644
index 00000000000..c936795f86c
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx
@@ -0,0 +1,86 @@
+/** @vitest-environment happy-dom */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import { clearHugeRepoWarningDismissalsForTests } from '@/lib/source-control-huge-repo-warning-dismissals'
+import { useSourceControlStatusRefresh } from './use-status-refresh'
+
+globalThis.IS_REACT_ACT_ENVIRONMENT = true
+
+const actions = vi.hoisted(() => ({
+ setGitStatus: vi.fn(),
+ updateWorktreeGitIdentity: vi.fn(),
+ setUpstreamStatus: vi.fn(),
+ fetchUpstreamStatus: vi.fn()
+}))
+const warning = vi.hoisted(() => vi.fn())
+vi.mock('@/store', () => ({
+ useAppStore: (select: (state: typeof actions) => unknown) => select(actions)
+}))
+vi.mock('sonner', () => ({ toast: { warning } }))
+vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
+vi.mock('../../git-status-refresh', () => ({ refreshGitStatusForWorktree: vi.fn() }))
+vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
+
+const findHugeFoldersToIgnore = vi.fn()
+const appendGitignore = vi.fn()
+let root: Root | null = null
+let container: HTMLDivElement | null = null
+
+beforeEach(() => {
+ findHugeFoldersToIgnore.mockReset().mockResolvedValue(['dist'])
+ appendGitignore.mockReset().mockResolvedValue(true)
+ warning.mockReset()
+ vi.stubGlobal('window', {
+ ...window,
+ api: { git: { findHugeFoldersToIgnore, appendGitignore } }
+ })
+ container = document.createElement('div')
+ root = createRoot(container)
+})
+
+afterEach(() => {
+ act(() => root?.unmount())
+ root = null
+ container = null
+ vi.unstubAllGlobals()
+ clearHugeRepoWarningDismissalsForTests()
+})
+
+async function renderOwner(
+ runtimeEnvironmentId: string | null,
+ connectionId: string | null = null
+) {
+ function Probe() {
+ useSourceControlStatusRefresh({
+ activeRepoSettings: { activeRuntimeEnvironmentId: runtimeEnvironmentId },
+ activeWorktreeId: 'repo::/same/path',
+ worktreePath: '/same/path',
+ isFolder: false,
+ repositoryHuge: { limit: 1000 },
+ activeConnectionId: connectionId,
+ worktreeMap: new Map()
+ })
+ return null
+ }
+ await act(async () => root?.render())
+}
+
+it('does not probe or offer a desktop ignore action for a managed owner without a legacy SSH ID', async () => {
+ await renderOwner('managed-owner')
+ expect(findHugeFoldersToIgnore).not.toHaveBeenCalled()
+ expect(warning).not.toHaveBeenCalled()
+ expect(appendGitignore).not.toHaveBeenCalled()
+})
+
+it('keeps the existing local suggestion', async () => {
+ await renderOwner(null)
+ expect(findHugeFoldersToIgnore).toHaveBeenCalledExactlyOnceWith({ worktreePath: '/same/path' })
+ expect(warning).toHaveBeenCalledTimes(1)
+})
+
+it('keeps direct SSH targets out of the desktop helper', async () => {
+ await renderOwner(null, 'ssh-owner')
+ expect(findHugeFoldersToIgnore).not.toHaveBeenCalled()
+ expect(warning).not.toHaveBeenCalled()
+})
diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts
index 46db77e99ab..d41b45b5019 100644
--- a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts
+++ b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect } from 'react'
import { toast } from 'sonner'
import { getConnectionId } from '@/lib/connection-context'
+import { isLocalPathOpenBlocked } from '@/lib/local-path-open-guard'
import {
beginHugeRepoWarningProbe,
hasDismissedHugeRepoWarning,
@@ -46,6 +47,9 @@ export function useSourceControlStatusRefresh({
const updateWorktreeGitIdentity = useAppStore((s) => s.updateWorktreeGitIdentity)
const setUpstreamStatus = useAppStore((s) => s.setUpstreamStatus)
const fetchUpstreamStatus = useAppStore((s) => s.fetchUpstreamStatus)
+ const localIgnoreBlocked = isLocalPathOpenBlocked(activeRepoSettings, {
+ connectionId: activeConnectionId
+ })
const refreshActiveGitStatus = useCallback(
async (signal?: AbortSignal): Promise => {
if (!activeWorktreeId || !worktreePath || isFolder) {
@@ -88,9 +92,9 @@ export function useSourceControlStatusRefresh({
}
}, [refreshActiveGitStatus])
- // Why: when status is truncated, offer once per worktree to .gitignore the flooding folder; local-only since the SSH huge-folder write path isn't wired.
+ // Why: when status is truncated, offer once per worktree to .gitignore the flooding folder; desktop-only since the probe and write run on this machine, and managed repos have no connectionId.
useEffect(() => {
- if (!repositoryHuge || !activeWorktreeId || !worktreePath || activeConnectionId) {
+ if (!repositoryHuge || !activeWorktreeId || !worktreePath || localIgnoreBlocked) {
return
}
const warningProbe = beginHugeRepoWarningProbe({
@@ -146,7 +150,7 @@ export function useSourceControlStatusRefresh({
activeWorktreeId,
activeWorktreeInstanceId,
worktreePath,
- activeConnectionId,
+ localIgnoreBlocked,
refreshActiveGitStatus
])