diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 8fc32d360f12b37f735cc9738603f3495960b32b..70f248b70574b1491be73691d4e2d7b8e0298e7a 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -19,8 +19,11 @@ import { useRef, useState, } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { gitBranchesQueryOptions } from "~/lib/gitReactQuery"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + gitBranchesQueryOptions, + gitCreateWorktreeMutationOptions, +} from "~/lib/gitReactQuery"; import { EDITORS, type EditorId } from "@t3tools/contracts"; import { isElectron } from "../env"; @@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise { export default function ChatView() { const { state, dispatch } = useStore(); const api = useNativeApi(); + const queryClient = useQueryClient(); + const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ api, queryClient })); const [prompt, setPrompt] = useState(""); const [composerImages, setComposerImages] = useState([]); const [isDragOverComposer, setIsDragOverComposer] = useState(false); @@ -663,7 +668,7 @@ export default function ChatView() { ) { try { const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`; - const result = await api.git.createWorktree({ + const result = await createWorktreeMutation.mutateAsync({ cwd: activeProject.cwd, branch: activeThread.branch, newBranch, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index dc26f95c96d860f83544e95857d689aea4013cb4..e851468280cf090ff252b2633d664425556fd190 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,5 +1,6 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; import { DEFAULT_MODEL } from "../model-logic"; @@ -12,6 +13,7 @@ import { type Thread, } from "../types"; import { useNativeApi } from "../hooks/useNativeApi"; +import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; @@ -110,6 +112,8 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null export default function Sidebar() { const { state, dispatch } = useStore(); const api = useNativeApi(); + const queryClient = useQueryClient(); + const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ api, queryClient })); const { theme, setTheme } = useTheme(); const [addingProject, setAddingProject] = useState(false); const [newCwd, setNewCwd] = useState(""); @@ -307,7 +311,7 @@ export default function Sidebar() { } try { - await api.git.removeWorktree({ + await removeWorktreeMutation.mutateAsync({ cwd: threadProject.cwd, path: orphanedWorktreePath, }); @@ -315,7 +319,7 @@ export default function Sidebar() { // Worktree deletion is best-effort and should not block thread deletion. } }, - [api, dispatch, state.projects, state.threads], + [api, dispatch, removeWorktreeMutation, state.projects, state.threads], ); useEffect(() => { diff --git a/apps/web/src/lib/gitReactQuery.ts b/apps/web/src/lib/gitReactQuery.ts index 1d9fd23f99a9d2f367a0277b1ca738571f1bec72..a051fa8a0423fe21f0d7582335e691331b984b63 100644 --- a/apps/web/src/lib/gitReactQuery.ts +++ b/apps/web/src/lib/gitReactQuery.ts @@ -108,8 +108,8 @@ export function gitRunStackedActionMutationOptions(input: { ...(commitMessage ? { commitMessage } : {}), }); }, - onSuccess: () => { - void invalidateGitQueries(input.queryClient); + onSettled: async () => { + await invalidateGitQueries(input.queryClient); }, }); } @@ -124,8 +124,50 @@ export function gitPullMutationOptions(input: { if (!input.api || !input.cwd) throw new Error("Git pull is unavailable."); return input.api.git.pull({ cwd: input.cwd }); }, - onSuccess: () => { - void invalidateGitQueries(input.queryClient); + onSettled: async () => { + await invalidateGitQueries(input.queryClient); + }, + }); +} + +export function gitCreateWorktreeMutationOptions(input: { + api: NativeApi | undefined; + queryClient: QueryClient; +}) { + return mutationOptions({ + mutationFn: async ({ + cwd, + branch, + newBranch, + }: { + cwd: string; + branch: string; + newBranch: string; + }) => { + if (!input.api) { + throw new Error("Git worktree creation is unavailable."); + } + return input.api.git.createWorktree({ cwd, branch, newBranch }); + }, + onSettled: async () => { + await invalidateGitQueries(input.queryClient); + }, + }); +} + +export function gitRemoveWorktreeMutationOptions(input: { + api: NativeApi | undefined; + queryClient: QueryClient; +}) { + return mutationOptions({ + mutationFn: async ({ cwd, path }: { cwd: string; path: string }) => { + if (!input.api) { + throw new Error("Git worktree removal is unavailable."); + } + return input.api.git.removeWorktree({ cwd, path }); + }, + onSettled: async () => { + await invalidateGitQueries(input.queryClient); }, }); }