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 {...@@ -19,8 +19,11 @@ import {
19 useRef,19 useRef,
20 useState,20 useState,
21} from "react";21} from "react";
22import { useQuery } from "@tanstack/react-query";22import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
23import { gitBranchesQueryOptions } from "~/lib/gitReactQuery";23import {
24 gitBranchesQueryOptions,
25 gitCreateWorktreeMutationOptions,
26} from "~/lib/gitReactQuery";
2427
25import { EDITORS, type EditorId } from "@t3tools/contracts";28import { EDITORS, type EditorId } from "@t3tools/contracts";
26import { isElectron } from "../env";29import { isElectron } from "../env";
...@@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise<string> {...@@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise<string> {
125export default function ChatView() {128export default function ChatView() {
126 const { state, dispatch } = useStore();129 const { state, dispatch } = useStore();
127 const api = useNativeApi();130 const api = useNativeApi();
131 const queryClient = useQueryClient();
132 const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ api, queryClient }));
128 const [prompt, setPrompt] = useState("");133 const [prompt, setPrompt] = useState("");
129 const [composerImages, setComposerImages] = useState<ComposerImageAttachment[]>([]);134 const [composerImages, setComposerImages] = useState<ComposerImageAttachment[]>([]);
130 const [isDragOverComposer, setIsDragOverComposer] = useState(false);135 const [isDragOverComposer, setIsDragOverComposer] = useState(false);
...@@ -663,7 +668,7 @@ export default function ChatView() {...@@ -663,7 +668,7 @@ export default function ChatView() {
663 ) {668 ) {
664 try {669 try {
665 const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`;670 const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`;
666 const result = await api.git.createWorktree({671 const result = await createWorktreeMutation.mutateAsync({
667 cwd: activeProject.cwd,672 cwd: activeProject.cwd,
668 branch: activeThread.branch,673 branch: activeThread.branch,
669 newBranch,674 newBranch,
apps/web/src/components/Sidebar.tsx+6-2
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react";1import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react";
2import { useCallback, useEffect, useMemo, useState } from "react";2import { useCallback, useEffect, useMemo, useState } from "react";
3import { useMutation, useQueryClient } from "@tanstack/react-query";
3import { isElectron } from "../env";4import { isElectron } from "../env";
4import { useTheme } from "../hooks/useTheme";5import { useTheme } from "../hooks/useTheme";
5import { DEFAULT_MODEL } from "../model-logic";6import { DEFAULT_MODEL } from "../model-logic";
...@@ -12,6 +13,7 @@ import {...@@ -12,6 +13,7 @@ import {
12 type Thread,13 type Thread,
13} from "../types";14} from "../types";
14import { useNativeApi } from "../hooks/useNativeApi";15import { useNativeApi } from "../hooks/useNativeApi";
16import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery";
15import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";17import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
1618
17const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;19const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
...@@ -110,6 +112,8 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null...@@ -110,6 +112,8 @@ function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null
110export default function Sidebar() {112export default function Sidebar() {
111 const { state, dispatch } = useStore();113 const { state, dispatch } = useStore();
112 const api = useNativeApi();114 const api = useNativeApi();
115 const queryClient = useQueryClient();
116 const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ api, queryClient }));
113 const { theme, setTheme } = useTheme();117 const { theme, setTheme } = useTheme();
114 const [addingProject, setAddingProject] = useState(false);118 const [addingProject, setAddingProject] = useState(false);
115 const [newCwd, setNewCwd] = useState("");119 const [newCwd, setNewCwd] = useState("");
...@@ -307,7 +311,7 @@ export default function Sidebar() {...@@ -307,7 +311,7 @@ export default function Sidebar() {
307 }311 }
308312
309 try {313 try {
310 await api.git.removeWorktree({314 await removeWorktreeMutation.mutateAsync({
311 cwd: threadProject.cwd,315 cwd: threadProject.cwd,
312 path: orphanedWorktreePath,316 path: orphanedWorktreePath,
313 });317 });
...@@ -315,7 +319,7 @@ export default function Sidebar() {...@@ -315,7 +319,7 @@ export default function Sidebar() {
315 // Worktree deletion is best-effort and should not block thread deletion.319 // Worktree deletion is best-effort and should not block thread deletion.
316 }320 }
317 },321 },
318 [api, dispatch, state.projects, state.threads],322 [api, dispatch, removeWorktreeMutation, state.projects, state.threads],
319 );323 );
320324
321 useEffect(() => {325 useEffect(() => {
apps/web/src/lib/gitReactQuery.ts+46-4
...@@ -108,8 +108,8 @@ export function gitRunStackedActionMutationOptions(input: {...@@ -108,8 +108,8 @@ export function gitRunStackedActionMutationOptions(input: {
108 ...(commitMessage ? { commitMessage } : {}),108 ...(commitMessage ? { commitMessage } : {}),
109 });109 });
110 },110 },
111 onSuccess: () => {111 onSettled: async () => {
112 void invalidateGitQueries(input.queryClient);112 await invalidateGitQueries(input.queryClient);
113 },113 },
114 });114 });
115}115}
...@@ -124,8 +124,50 @@ export function gitPullMutationOptions(input: {...@@ -124,8 +124,50 @@ export function gitPullMutationOptions(input: {
124 if (!input.api || !input.cwd) throw new Error("Git pull is unavailable.");124 if (!input.api || !input.cwd) throw new Error("Git pull is unavailable.");
125 return input.api.git.pull({ cwd: input.cwd });125 return input.api.git.pull({ cwd: input.cwd });
126 },126 },
127 onSuccess: () => {127 onSettled: async () => {
128 void invalidateGitQueries(input.queryClient);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);
129 },171 },
130 });172 });
131}173}