fix(ssh): rebind file watcher when connection id hydrates

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-05-11 17:58:17 -04:00
co-authored by Orca
parent 098f591544
commit 271f053a83
2 changed files with 35 additions and 8 deletions
@@ -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()
@@ -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({