diff --git a/src/renderer/src/hooks/useEditorExternalWatch.test.ts b/src/renderer/src/hooks/useEditorExternalWatch.test.ts index d8f080e71c3..f6f809ff472 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.test.ts @@ -16,10 +16,29 @@ vi.mock('@/components/editor/editor-autosave', () => ({ import { createExternalWatchEventHandler, - getOverflowExternalReloadTargets + getOverflowExternalReloadTargets, + getWatchedTargetKey } from './useEditorExternalWatch' import { useAppStore } from '@/store' +describe('getWatchedTargetKey', () => { + it('changes when a worktree gains an SSH connection id', () => { + expect( + getWatchedTargetKey({ + worktreeId: 'wt-1', + worktreePath: '/repo', + connectionId: undefined + }) + ).not.toBe( + getWatchedTargetKey({ + worktreeId: 'wt-1', + worktreePath: '/repo', + connectionId: 'conn-1' + }) + ) + }) +}) + describe('getOverflowExternalReloadTargets', () => { const setExternalMutation = vi.fn() diff --git a/src/renderer/src/hooks/useEditorExternalWatch.ts b/src/renderer/src/hooks/useEditorExternalWatch.ts index 416ef1913b4..2c28aae092d 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.ts @@ -66,6 +66,13 @@ type ExternalWatchNotification = { relativePath: string } +export function getWatchedTargetKey(target: WatchedTarget): string { + // Why: SSH worktrees can exist in the store before their remote filesystem + // provider is ready. Include connectionId so a local/unknown placeholder + // watch is replaced by the real SSH watch when the repo metadata hydrates. + return `${target.worktreeId}::${target.worktreePath}::${target.connectionId ?? 'local'}` +} + // Why: macOS atomic writes (Claude Code Edit, vim :w, VSCode save) deliver a // delete event immediately followed by a create event for the same path. When // those two land in separate fs:changed payloads a few ms apart, the tab @@ -123,12 +130,13 @@ export function useEditorExternalWatch(): void { if (!wt) { continue } - nextTargets.push({ + const target = { worktreeId: id, worktreePath: wt.path, connectionId: getConnectionId(id) ?? undefined - }) - parts.push(`${id}::${wt.path}`) + } + nextTargets.push(target) + parts.push(getWatchedTargetKey(target)) } return { targets: nextTargets, targetsKey: parts.join('|') } }, [openFiles, worktreesByRepo, activeWorktreeId]) @@ -144,10 +152,10 @@ export function useEditorExternalWatch(): void { useEffect(() => { const nextTargets = latestTargetsRef.current const prev = targetsRef.current - const prevIds = new Set(prev.map((t) => t.worktreeId)) - const nextIds = new Set(nextTargets.map((t) => t.worktreeId)) - const removed = prev.filter((t) => !nextIds.has(t.worktreeId)) - const added = nextTargets.filter((t) => !prevIds.has(t.worktreeId)) + const prevKeys = new Set(prev.map(getWatchedTargetKey)) + const nextKeys = new Set(nextTargets.map(getWatchedTargetKey)) + const removed = prev.filter((t) => !nextKeys.has(getWatchedTargetKey(t))) + const added = nextTargets.filter((t) => !prevKeys.has(getWatchedTargetKey(t))) for (const target of removed) { void window.api.fs.unwatchWorktree({