| author | |
| committer | |
| log | a6b9ece840989e615e3cfc443524d71f4cac6f42 |
| tree | 59c0831a2c20c9268b952bb80cd3f36bf67675c8 |
| parent | 65ab532342a24e8629b2feb9ce514ae45092866c |
- 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 actions3 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"; |
| 22 | import { useQuery } from "@tanstack/react-query"; | 22 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 23 | import { gitBranchesQueryOptions } from "~/lib/gitReactQuery"; | 23 | import { |
| 24 | gitBranchesQueryOptions, | ||
| 25 | gitCreateWorktreeMutationOptions, | ||
| 26 | } from "~/lib/gitReactQuery"; | ||
| 24 | 27 | ||
| 25 | import { EDITORS, type EditorId } from "@t3tools/contracts"; | 28 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 26 | import { isElectron } from "../env"; | 29 | import { isElectron } from "../env"; |
| ... | @@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise<string> { | ... | @@ -125,6 +128,8 @@ function readFileAsDataUrl(file: File): Promise<string> { |
| 125 | export default function ChatView() { | 128 | export 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 @@ |
| 1 | import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; | 1 | import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useMemo, useState } from "react"; | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
| 3 | import { isElectron } from "../env"; | 4 | import { isElectron } from "../env"; |
| 4 | import { useTheme } from "../hooks/useTheme"; | 5 | import { useTheme } from "../hooks/useTheme"; |
| 5 | import { DEFAULT_MODEL } from "../model-logic"; | 6 | import { 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"; |
| 14 | import { useNativeApi } from "../hooks/useNativeApi"; | 15 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 16 | import { gitRemoveWorktreeMutationOptions } from "../lib/gitReactQuery"; | ||
| 15 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; | 17 | import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; |
| 16 | 18 | ||
| 17 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | 19 | const 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 |
| 110 | export default function Sidebar() { | 112 | export 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 | } |
| 308 | 312 | ||
| 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 | ); |
| 320 | 324 | ||
| 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 | |||
| 133 | export 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 | |||
| 158 | export 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 | } |