authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 00:00:41-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 00:00:41-08:00
loga6b9ece840989e615e3cfc443524d71f4cac6f42
tree59c0831a2c20c9268b952bb80cd3f36bf67675c8
parent65ab532342a24e8629b2feb9ce514ae45092866c

Use React Query mutations for git worktree operations

- Route worktree create/remove calls through shared git mutation options - Invalidate git queries on mutation settle to keep branch/worktree data fresh - Update chat and sidebar flows to use `mutateAsync` for worktree actions

3 files changed, 60 insertions(+), 9 deletions(-)

apps/web/src/components/ChatView.tsx+8-3
......@@ -19,8 +19,11 @@ import {
1919 useRef,
2020 useState,
2121} from "react";
22import { useQuery } from "@tanstack/react-query";
23import { gitBranchesQueryOptions } from "~/lib/gitReactQuery";
22import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
23import {
24 gitBranchesQueryOptions,
25 gitCreateWorktreeMutationOptions,
26} from "~/lib/gitReactQuery";
2427
2528import { EDITORS, type EditorId } from "@t3tools/contracts";
2629import { isElectron } from "../env";
......@@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise<string> {
125128export default function ChatView() {
126129 const { state, dispatch } = useStore();
127130 const api = useNativeApi();
131 const queryClient = useQueryClient();
132 const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ api, queryClient }));
128133 const [prompt, setPrompt] = useState("");
129134 const [composerImages, setComposerImages] = useState<ComposerImageAttachment[]>([]);
130135 const [isDragOverComposer, setIsDragOverComposer] = useState(false);
......@@ -663,7 +668,7 @@ export default function ChatView() {
663668 ) {
664669 try {
665670 const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`;
666 const result = await api.git.createWorktree({
671 const result = await createWorktreeMutation.mutateAsync({
667672 cwd: activeProject.cwd,
668673 branch: activeThread.branch,
669674 newBranch,
apps/web/src/components/Sidebar.tsx+6-2
......@@ -1,5 +1,6 @@
11import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react";
22import { useCallback, useEffect, useMemo, useState } from "react";
3import { useMutation, useQueryClient } from "@tanstack/react-query";
34import { isElectron } from "../env";
45import { useTheme } from "../hooks/useTheme";
56import { DEFAULT_MODEL } from "../model-logic";
......@@ -12,6 +13,7 @@ import {
1213 type Thread,
1314} from "../types";
1415import { useNativeApi } from "../hooks/useNativeApi";
16import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";
1517import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
1618
1719const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
......@@ -110,6 +112,8 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null
110112export default function Sidebar() {
111113 const { state, dispatch } = useStore();
112114 const api = useNativeApi();
115 const queryClient = useQueryClient();
116 const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ api, queryClient }));
113117 const { theme, setTheme } = useTheme();
114118 const [addingProject, setAddingProject] = useState(false);
115119 const [newCwd, setNewCwd] = useState("");
......@@ -307,7 +311,7 @@ export default function Sidebar() {
307311 }
308312
309313 try {
310 await api.git.removeWorktree({
314 await removeWorktreeMutation.mutateAsync({
311315 cwd: threadProject.cwd,
312316 path: orphanedWorktreePath,
313317 });
......@@ -315,7 +319,7 @@ export default function Sidebar() {
315319 // Worktree deletion is best-effort and should not block thread deletion.
316320 }
317321 },
318 [api, dispatch, state.projects, state.threads],
322 [api, dispatch, removeWorktreeMutation, state.projects, state.threads],
319323 );
320324
321325 useEffect(() => {
apps/web/src/lib/gitReactQuery.ts+46-4
......@@ -108,8 +108,8 @@ export function gitRunStackedActionMutationOptions(input: {
108108 ...(commitMessage ? { commitMessage } : {}),
109109 });
110110 },
111 onSuccess: () => {
112 void invalidateGitQueries(input.queryClient);
111 onSettled: async () => {
112 await invalidateGitQueries(input.queryClient);
113113 },
114114 });
115115}
......@@ -124,8 +124,50 @@ export function gitPullMutationOptions(input: {
124124 if (!input.api || !input.cwd) throw new Error("Git pull is unavailable.");
125125 return input.api.git.pull({ cwd: input.cwd });
126126 },
127 onSuccess: () => {
128 void invalidateGitQueries(input.queryClient);
127 onSettled: async () => {
128 await invalidateGitQueries(input.queryClient);
129 },
130 });
131}
132
133export function gitCreateWorktreeMutationOptions(input: {
134 api: NativeApi | undefined;
135 queryClient: QueryClient;
136}) {
137 return mutationOptions({
138 mutationFn: async ({
139 cwd,
140 branch,
141 newBranch,
142 }: {
143 cwd: string;
144 branch: string;
145 newBranch: string;
146 }) => {
147 if (!input.api) {
148 throw new Error("Git worktree creation is unavailable.");
149 }
150 return input.api.git.createWorktree({ cwd, branch, newBranch });
151 },
152 onSettled: async () => {
153 await invalidateGitQueries(input.queryClient);
154 },
155 });
156}
157
158export function gitRemoveWorktreeMutationOptions(input: {
159 api: NativeApi | undefined;
160 queryClient: QueryClient;
161}) {
162 return mutationOptions({
163 mutationFn: async ({ cwd, path }: { cwd: string; path: string }) => {
164 if (!input.api) {
165 throw new Error("Git worktree removal is unavailable.");
166 }
167 return input.api.git.removeWorktree({ cwd, path });
168 },
169 onSettled: async () => {
170 await invalidateGitQueries(input.queryClient);
129171 },
130172 });
131173}