authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 18:43:24-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 19:22:47-08:00
log1b98213d6d93df663b91774a5f18e94a9a1906af
tree80c262933eb2b0df1a964aaac5ea80ffcc4f6f05
parent0d67424f6945fb451e9b7639861cdac8b18a0c3b

Scope git mutation state by cwd for action loading UI

- add `gitMutationKeys` and attach mutation keys across git mutation options - switch GitActionsControl loading state to `useIsMutating` with cwd-scoped keys - add tests for cwd-scoped mutation keys and mutation option wiring

3 files changed, 56 insertions(+), 2 deletions(-)

apps/web/src/components/GitActionsControl.tsx+6-2
......@@ -1,5 +1,5 @@
11import { type GitStatusResult, type GitStackedAction, type ThreadId } from "@t3tools/contracts";
2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
33import { useCallback, useEffect, useMemo, useState } from "react";
44import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react";
55import { GitHubIcon } from "./Icons";
......@@ -33,6 +33,7 @@ import { toastManager } from "~/components/ui/toast";
3333import {
3434 gitBranchesQueryOptions,
3535 gitInitMutationOptions,
36 gitMutationKeys,
3637 gitPullMutationOptions,
3738 gitRunStackedActionMutationOptions,
3839 gitStatusQueryOptions,
......@@ -168,7 +169,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
168169 );
169170 const pullMutation = useMutation(gitPullMutationOptions({ cwd: gitCwd, queryClient }));
170171
171 const isGitActionRunning = runImmediateGitActionMutation.isPending || pullMutation.isPending;
172 const isRunStackedActionRunning =
173 useIsMutating({ mutationKey: gitMutationKeys.runStackedAction(gitCwd) }) > 0;
174 const isPullRunning = useIsMutating({ mutationKey: gitMutationKeys.pull(gitCwd) }) > 0;
175 const isGitActionRunning = isRunStackedActionRunning || isPullRunning;
172176 const isDefaultBranch = useMemo(() => {
173177 const branchName = gitStatusForActions?.branch;
174178 if (!branchName) return false;
apps/web/src/lib/gitReactQuery.test.ts created+33
......@@ -0,0 +1,33 @@
1import { QueryClient } from "@tanstack/react-query";
2import { describe, expect, it } from "vitest";
3import {
4 gitMutationKeys,
5 gitPullMutationOptions,
6 gitRunStackedActionMutationOptions,
7} from "./gitReactQuery";
8
9describe("gitMutationKeys", () => {
10 it("scopes stacked action keys by cwd", () => {
11 expect(gitMutationKeys.runStackedAction("/repo/a")).not.toEqual(
12 gitMutationKeys.runStackedAction("/repo/b"),
13 );
14 });
15
16 it("scopes pull keys by cwd", () => {
17 expect(gitMutationKeys.pull("/repo/a")).not.toEqual(gitMutationKeys.pull("/repo/b"));
18 });
19});
20
21describe("git mutation options", () => {
22 const queryClient = new QueryClient();
23
24 it("attaches cwd-scoped mutation key for runStackedAction", () => {
25 const options = gitRunStackedActionMutationOptions({ cwd: "/repo/a", queryClient });
26 expect(options.mutationKey).toEqual(gitMutationKeys.runStackedAction("/repo/a"));
27 });
28
29 it("attaches cwd-scoped mutation key for pull", () => {
30 const options = gitPullMutationOptions({ cwd: "/repo/a", queryClient });
31 expect(options.mutationKey).toEqual(gitMutationKeys.pull("/repo/a"));
32 });
33});
apps/web/src/lib/gitReactQuery.ts+17
......@@ -13,6 +13,16 @@ export const gitQueryKeys = {
1313 branches: (cwd: string | null) => ["git", "branches", cwd] as const,
1414};
1515
16export const gitMutationKeys = {
17 init: (cwd: string | null) => ["git", "mutation", "init", cwd] as const,
18 checkout: (cwd: string | null) => ["git", "mutation", "checkout", cwd] as const,
19 createBranchAndCheckout: (cwd: string | null) =>
20 ["git", "mutation", "create-branch-and-checkout", cwd] as const,
21 runStackedAction: (cwd: string | null) =>
22 ["git", "mutation", "run-stacked-action", cwd] as const,
23 pull: (cwd: string | null) => ["git", "mutation", "pull", cwd] as const,
24};
25
1626export function invalidateGitQueries(queryClient: QueryClient) {
1727 return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all });
1828}
......@@ -51,6 +61,7 @@ export function gitBranchesQueryOptions(cwd: string | null) {
5161
5262export function gitInitMutationOptions(input: { cwd: string | null; queryClient: QueryClient }) {
5363 return mutationOptions({
64 mutationKey: gitMutationKeys.init(input.cwd),
5465 mutationFn: async () => {
5566 const api = ensureNativeApi();
5667 if (!input.cwd) throw new Error("Git init is unavailable.");
......@@ -67,6 +78,7 @@ export function gitCheckoutMutationOptions(input: {
6778 queryClient: QueryClient;
6879}) {
6980 return mutationOptions({
81 mutationKey: gitMutationKeys.checkout(input.cwd),
7082 mutationFn: async (branch: string) => {
7183 const api = ensureNativeApi();
7284 if (!input.cwd) throw new Error("Git checkout is unavailable.");
......@@ -83,6 +95,7 @@ export function gitCreateBranchAndCheckoutMutationOptions(input: {
8395 queryClient: QueryClient;
8496}) {
8597 return mutationOptions({
98 mutationKey: gitMutationKeys.createBranchAndCheckout(input.cwd),
8699 mutationFn: async (branch: string) => {
87100 const api = ensureNativeApi();
88101 if (!input.cwd) throw new Error("Git branch creation is unavailable.");
......@@ -100,6 +113,7 @@ export function gitRunStackedActionMutationOptions(input: {
100113 queryClient: QueryClient;
101114}) {
102115 return mutationOptions({
116 mutationKey: gitMutationKeys.runStackedAction(input.cwd),
103117 mutationFn: async ({
104118 action,
105119 commitMessage,
......@@ -123,6 +137,7 @@ export function gitRunStackedActionMutationOptions(input: {
123137
124138export function gitPullMutationOptions(input: { cwd: string | null; queryClient: QueryClient }) {
125139 return mutationOptions({
140 mutationKey: gitMutationKeys.pull(input.cwd),
126141 mutationFn: async () => {
127142 const api = ensureNativeApi();
128143 if (!input.cwd) throw new Error("Git pull is unavailable.");
......@@ -151,6 +166,7 @@ export function gitCreateWorktreeMutationOptions(input: { queryClient: QueryClie
151166 if (!cwd) throw new Error("Git worktree creation is unavailable.");
152167 return api.git.createWorktree({ cwd, branch, newBranch, path: path ?? null });
153168 },
169 mutationKey: ["git", "mutation", "create-worktree"] as const,
154170 onSettled: async () => {
155171 await invalidateGitQueries(input.queryClient);
156172 },
......@@ -164,6 +180,7 @@ export function gitRemoveWorktreeMutationOptions(input: { queryClient: QueryClie
164180 if (!cwd) throw new Error("Git worktree removal is unavailable.");
165181 return api.git.removeWorktree({ cwd, path, force });
166182 },
183 mutationKey: ["git", "mutation", "remove-worktree"] as const,
167184 onSettled: async () => {
168185 await invalidateGitQueries(input.queryClient);
169186 },