authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 16:27:07-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 18:53:21-08:00
logb81ec02984c92c9506c7859ab5c967f3b8afae1d
tree0f206cc948acfd0d6ba3907405e439c7fdc52770
parentde61c0c42b0797735a5cbe7ec6d4b3358fcf4295

Show optimistic active branch during checkout and branch creation

- use `useOptimistic` in `BranchToolbar` to update active branch immediately - keep optimistic branch in sync on success and roll back on mutation errors

1 files changed, 16 insertions(+), 6 deletions(-)

apps/web/src/components/BranchToolbar.tsx+16-6
......@@ -1,6 +1,6 @@
11import type { GitBranch, ThreadId } from "@t3tools/contracts";
22import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3import { useState } from "react";
3import { useOptimistic, useState } from "react";
44
55import { newCommandId } from "../lib/utils";
66import {
......@@ -72,10 +72,14 @@ export default function BranchToolbar({
7272
7373 const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []);
7474 const currentGitBranch = branches.find((branch) => branch.current)?.name ?? null;
75 const resolvedActiveBranch =
75 const canonicalActiveBranch =
7676 effectiveEnvMode === "worktree" && !activeWorktreePath
7777 ? activeThreadBranch
7878 : (currentGitBranch ?? activeThreadBranch);
79 const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(
80 canonicalActiveBranch,
81 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
82 );
7983 const branchNames = branches.map((branch) => branch.name);
8084 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
8185 const trimmedBranchQuery = branchQuery.trim();
......@@ -168,6 +172,7 @@ export default function BranchToolbar({
168172 const selectedBranchName = branch.isRemote
169173 ? deriveLocalBranchNameFromRemoteRef(branch.name)
170174 : branch.name;
175 setOptimisticBranch(selectedBranchName);
171176
172177 checkoutMutation.mutate(branch.name, {
173178 onSuccess: async () => {
......@@ -179,11 +184,13 @@ export default function BranchToolbar({
179184 nextBranchName = status.branch;
180185 }
181186 }
187 setOptimisticBranch(nextBranchName);
182188 setThreadBranch(nextBranchName, activeWorktreePath);
183189 setIsBranchMenuOpen(false);
184190 onComposerFocusRequest?.();
185191 },
186192 onError: (error) => {
193 setOptimisticBranch(canonicalActiveBranch);
187194 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
188195 setIsBranchMenuOpen(true);
189196 },
......@@ -194,8 +201,10 @@ export default function BranchToolbar({
194201 const name = rawName.trim();
195202 const api = readNativeApi();
196203 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;
204 setOptimisticBranch(name);
197205 createBranchMutation.mutate(name, {
198206 onSuccess: () => {
207 setOptimisticBranch(name);
199208 setThreadError(null);
200209 setThreadBranch(name, activeWorktreePath);
201210 setBranchQuery("");
......@@ -203,6 +212,7 @@ export default function BranchToolbar({
203212 onComposerFocusRequest?.();
204213 },
205214 onError: (error) => {
215 setOptimisticBranch(canonicalActiveBranch);
206216 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
207217 },
208218 });
......@@ -223,9 +233,7 @@ export default function BranchToolbar({
223233 variant="ghost"
224234 className="text-muted-foreground/70 hover:text-foreground/80"
225235 size="xs"
226 onClick={() =>
227 onEnvModeChange(effectiveEnvMode === "local" ? "worktree" : "local")
228 }
236 onClick={() => onEnvModeChange(effectiveEnvMode === "local" ? "worktree" : "local")}
229237 >
230238 {effectiveEnvMode === "worktree" ? "New worktree" : "Local"}
231239 </Button>
......@@ -313,7 +321,9 @@ export default function BranchToolbar({
313321 >
314322 <div className="flex w-full items-center justify-between gap-2">
315323 <span className="truncate">{itemValue}</span>
316 {badge && <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>}
324 {badge && (
325 <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>
326 )}
317327 </div>
318328 </ComboboxItem>
319329 );