diff --git a/apps/code/src/main/services/file-watcher/schemas.ts b/apps/code/src/main/services/file-watcher/schemas.ts index 3b11e9522..f4072853a 100644 --- a/apps/code/src/main/services/file-watcher/schemas.ts +++ b/apps/code/src/main/services/file-watcher/schemas.ts @@ -25,6 +25,7 @@ export const FileWatcherEvent = { FileChanged: "file-changed", FileDeleted: "file-deleted", GitStateChanged: "git-state-changed", + WorkingTreeChanged: "working-tree-changed", } as const; export type DirectoryChangedPayload = { @@ -46,9 +47,14 @@ export type GitStateChangedPayload = { repoPath: string; }; +export type WorkingTreeChangedPayload = { + repoPath: string; +}; + export interface FileWatcherEvents { [FileWatcherEvent.DirectoryChanged]: DirectoryChangedPayload; [FileWatcherEvent.FileChanged]: FileChangedPayload; [FileWatcherEvent.FileDeleted]: FileDeletedPayload; [FileWatcherEvent.GitStateChanged]: GitStateChangedPayload; + [FileWatcherEvent.WorkingTreeChanged]: WorkingTreeChangedPayload; } diff --git a/apps/code/src/main/services/file-watcher/service.ts b/apps/code/src/main/services/file-watcher/service.ts index 3a54ccdb2..e86e5b486 100644 --- a/apps/code/src/main/services/file-watcher/service.ts +++ b/apps/code/src/main/services/file-watcher/service.ts @@ -171,9 +171,11 @@ export class FileWatcherService extends TypedEventEmitter { const totalChanges = pending.files.size + pending.deletes.size; - // For bulk changes, emit a single event instead of per-file events + if (totalChanges > 0) { + this.emit(FileWatcherEvent.WorkingTreeChanged, { repoPath }); + } + if (totalChanges > BULK_THRESHOLD) { - this.emit(FileWatcherEvent.GitStateChanged, { repoPath }); pending.dirs.clear(); pending.files.clear(); pending.deletes.clear(); diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 68084b985..a45d2eb9d 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -297,20 +297,29 @@ export class GitService extends TypedEventEmitter { return getRemoteUrl(directoryPath); } - public async getCurrentBranch(directoryPath: string): Promise { - return getCurrentBranch(directoryPath); + public async getCurrentBranch( + directoryPath: string, + signal?: AbortSignal, + ): Promise { + return getCurrentBranch(directoryPath, { abortSignal: signal }); } public async getDefaultBranch(directoryPath: string): Promise { return getDefaultBranch(directoryPath); } - public async getAllBranches(directoryPath: string): Promise { - return getAllBranches(directoryPath); + public async getAllBranches( + directoryPath: string, + signal?: AbortSignal, + ): Promise { + return getAllBranches(directoryPath, { abortSignal: signal }); } - public async getGitBusyState(directoryPath: string): Promise { - return getGitBusyState(directoryPath); + public async getGitBusyState( + directoryPath: string, + signal?: AbortSignal, + ): Promise { + return getGitBusyState(directoryPath, { abortSignal: signal }); } public async createBranch( @@ -334,9 +343,11 @@ export class GitService extends TypedEventEmitter { public async getChangedFilesHead( directoryPath: string, + signal?: AbortSignal, ): Promise { const files = await getChangedFilesDetailed(directoryPath, { excludePatterns: [".claude", "CLAUDE.local.md"], + abortSignal: signal, }); type HeadChangedFile = Omit; const filteredFiles: Array = await Promise.all( @@ -371,29 +382,42 @@ export class GitService extends TypedEventEmitter { public async getFileAtHead( directoryPath: string, filePath: string, + signal?: AbortSignal, ): Promise { - return getFileAtHead(directoryPath, filePath); + return getFileAtHead(directoryPath, filePath, { abortSignal: signal }); } public async getDiffHead( directoryPath: string, ignoreWhitespace?: boolean, + signal?: AbortSignal, ): Promise { - return getDiffHead(directoryPath, { ignoreWhitespace }); + return getDiffHead(directoryPath, { + ignoreWhitespace, + abortSignal: signal, + }); } public async getDiffCached( directoryPath: string, ignoreWhitespace?: boolean, + signal?: AbortSignal, ): Promise { - return getStagedDiff(directoryPath, { ignoreWhitespace }); + return getStagedDiff(directoryPath, { + ignoreWhitespace, + abortSignal: signal, + }); } public async getDiffUnstaged( directoryPath: string, ignoreWhitespace?: boolean, + signal?: AbortSignal, ): Promise { - return getUnstagedDiff(directoryPath, { ignoreWhitespace }); + return getUnstagedDiff(directoryPath, { + ignoreWhitespace, + abortSignal: signal, + }); } public async stageFiles( @@ -412,9 +436,13 @@ export class GitService extends TypedEventEmitter { return this.getStateSnapshot(directoryPath); } - public async getDiffStats(directoryPath: string): Promise { + public async getDiffStats( + directoryPath: string, + signal?: AbortSignal, + ): Promise { const stats = await getDiffStats(directoryPath, { excludePatterns: [".claude", "CLAUDE.local.md"], + abortSignal: signal, }); return { filesChanged: stats.filesChanged, @@ -455,8 +483,11 @@ export class GitService extends TypedEventEmitter { public async getLatestCommit( directoryPath: string, + signal?: AbortSignal, ): Promise { - const commit = await getLatestCommit(directoryPath); + const commit = await getLatestCommit(directoryPath, { + abortSignal: signal, + }); if (!commit) return null; return { sha: commit.sha, diff --git a/apps/code/src/main/trpc/routers/file-watcher.ts b/apps/code/src/main/trpc/routers/file-watcher.ts index 92d90ecdb..c442556d1 100644 --- a/apps/code/src/main/trpc/routers/file-watcher.ts +++ b/apps/code/src/main/trpc/routers/file-watcher.ts @@ -41,4 +41,5 @@ export const fileWatcherRouter = router({ onFileChanged: subscribe(FileWatcherEvent.FileChanged), onFileDeleted: subscribe(FileWatcherEvent.FileDeleted), onGitStateChanged: subscribe(FileWatcherEvent.GitStateChanged), + onWorkingTreeChanged: subscribe(FileWatcherEvent.WorkingTreeChanged), }); diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 5137ed435..b364d7aad 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -125,17 +125,23 @@ export const gitRouter = router({ getCurrentBranch: publicProcedure .input(getCurrentBranchInput) .output(getCurrentBranchOutput) - .query(({ input }) => getService().getCurrentBranch(input.directoryPath)), + .query(({ input, signal }) => + getService().getCurrentBranch(input.directoryPath, signal), + ), getAllBranches: publicProcedure .input(getAllBranchesInput) .output(getAllBranchesOutput) - .query(({ input }) => getService().getAllBranches(input.directoryPath)), + .query(({ input, signal }) => + getService().getAllBranches(input.directoryPath, signal), + ), getGitBusyState: publicProcedure .input(getGitBusyStateInput) .output(getGitBusyStateOutput) - .query(({ input }) => getService().getGitBusyState(input.directoryPath)), + .query(({ input, signal }) => + getService().getGitBusyState(input.directoryPath, signal), + ), createBranch: publicProcedure .input(createBranchInput) @@ -154,42 +160,56 @@ export const gitRouter = router({ getChangedFilesHead: publicProcedure .input(getChangedFilesHeadInput) .output(getChangedFilesHeadOutput) - .query(({ input }) => - getService().getChangedFilesHead(input.directoryPath), + .query(({ input, signal }) => + getService().getChangedFilesHead(input.directoryPath, signal), ), getFileAtHead: publicProcedure .input(getFileAtHeadInput) .output(getFileAtHeadOutput) - .query(({ input }) => - getService().getFileAtHead(input.directoryPath, input.filePath), + .query(({ input, signal }) => + getService().getFileAtHead(input.directoryPath, input.filePath, signal), ), getDiffHead: publicProcedure .input(diffInput) .output(diffOutput) - .query(({ input }) => - getService().getDiffHead(input.directoryPath, input.ignoreWhitespace), + .query(({ input, signal }) => + getService().getDiffHead( + input.directoryPath, + input.ignoreWhitespace, + signal, + ), ), getDiffCached: publicProcedure .input(diffInput) .output(diffOutput) - .query(({ input }) => - getService().getDiffCached(input.directoryPath, input.ignoreWhitespace), + .query(({ input, signal }) => + getService().getDiffCached( + input.directoryPath, + input.ignoreWhitespace, + signal, + ), ), getDiffUnstaged: publicProcedure .input(diffInput) .output(diffOutput) - .query(({ input }) => - getService().getDiffUnstaged(input.directoryPath, input.ignoreWhitespace), + .query(({ input, signal }) => + getService().getDiffUnstaged( + input.directoryPath, + input.ignoreWhitespace, + signal, + ), ), getDiffStats: publicProcedure .input(getDiffStatsInput) .output(getDiffStatsOutput) - .query(({ input }) => getService().getDiffStats(input.directoryPath)), + .query(({ input, signal }) => + getService().getDiffStats(input.directoryPath, signal), + ), stageFiles: publicProcedure .input(stageFilesInput) @@ -233,7 +253,9 @@ export const gitRouter = router({ getLatestCommit: publicProcedure .input(getLatestCommitInput) .output(getLatestCommitOutput) - .query(({ input }) => getService().getLatestCommit(input.directoryPath)), + .query(({ input, signal }) => + getService().getLatestCommit(input.directoryPath, signal), + ), getGitRepoInfo: publicProcedure .input(getGitRepoInfoInput) diff --git a/apps/code/src/renderer/hooks/useFileWatcher.ts b/apps/code/src/renderer/hooks/useFileWatcher.ts index 44604f15e..c8134f171 100644 --- a/apps/code/src/renderer/hooks/useFileWatcher.ts +++ b/apps/code/src/renderer/hooks/useFileWatcher.ts @@ -47,7 +47,6 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) { filePath: relativePath, }), ); - invalidateGitWorkingTreeQueries(repoPath); }, }), ); @@ -57,7 +56,6 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) { enabled: !!repoPath, onData: ({ repoPath: rp, filePath }) => { if (rp !== repoPath) return; - invalidateGitWorkingTreeQueries(repoPath); if (!taskId) return; const relativePath = toRelativePath(filePath, repoPath); closeTabsForFile(taskId, relativePath); @@ -71,6 +69,15 @@ export function useFileWatcher(repoPath: string | null, taskId?: string) { onData: ({ repoPath: rp }) => { if (rp !== repoPath) return; invalidateGitBranchQueries(repoPath); + }, + }), + ); + + useSubscription( + trpc.fileWatcher.onWorkingTreeChanged.subscriptionOptions(undefined, { + enabled: !!repoPath, + onData: ({ repoPath: rp }) => { + if (rp !== repoPath) return; invalidateGitWorkingTreeQueries(repoPath); }, }),