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 @@...@@ -1,6 +1,6 @@
1import type { GitBranch, ThreadId } from "@t3tools/contracts";1import type { GitBranch, ThreadId } from "@t3tools/contracts";
2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3import { useState } from "react";3import { useOptimistic, useState } from "react";
44
5import { newCommandId } from "../lib/utils";5import { newCommandId } from "../lib/utils";
6import {6import {
...@@ -72,10 +72,14 @@ export default function BranchToolbar({...@@ -72,10 +72,14 @@ export default function BranchToolbar({
7272
73 const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []);73 const branches = dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []);
74 const currentGitBranch = branches.find((branch) => branch.current)?.name ?? null;74 const currentGitBranch = branches.find((branch) => branch.current)?.name ?? null;
75 const resolvedActiveBranch =75 const canonicalActiveBranch =
76 effectiveEnvMode === "worktree" && !activeWorktreePath76 effectiveEnvMode === "worktree" && !activeWorktreePath
77 ? activeThreadBranch77 ? activeThreadBranch
78 : (currentGitBranch ?? activeThreadBranch);78 : (currentGitBranch ?? activeThreadBranch);
79 const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(
80 canonicalActiveBranch,
81 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
82 );
79 const branchNames = branches.map((branch) => branch.name);83 const branchNames = branches.map((branch) => branch.name);
80 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));84 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
81 const trimmedBranchQuery = branchQuery.trim();85 const trimmedBranchQuery = branchQuery.trim();
...@@ -168,6 +172,7 @@ export default function BranchToolbar({...@@ -168,6 +172,7 @@ export default function BranchToolbar({
168 const selectedBranchName = branch.isRemote172 const selectedBranchName = branch.isRemote
169 ? deriveLocalBranchNameFromRemoteRef(branch.name)173 ? deriveLocalBranchNameFromRemoteRef(branch.name)
170 : branch.name;174 : branch.name;
175 setOptimisticBranch(selectedBranchName);
171176
172 checkoutMutation.mutate(branch.name, {177 checkoutMutation.mutate(branch.name, {
173 onSuccess: async () => {178 onSuccess: async () => {
...@@ -179,11 +184,13 @@ export default function BranchToolbar({...@@ -179,11 +184,13 @@ export default function BranchToolbar({
179 nextBranchName = status.branch;184 nextBranchName = status.branch;
180 }185 }
181 }186 }
187 setOptimisticBranch(nextBranchName);
182 setThreadBranch(nextBranchName, activeWorktreePath);188 setThreadBranch(nextBranchName, activeWorktreePath);
183 setIsBranchMenuOpen(false);189 setIsBranchMenuOpen(false);
184 onComposerFocusRequest?.();190 onComposerFocusRequest?.();
185 },191 },
186 onError: (error) => {192 onError: (error) => {
193 setOptimisticBranch(canonicalActiveBranch);
187 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");194 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
188 setIsBranchMenuOpen(true);195 setIsBranchMenuOpen(true);
189 },196 },
...@@ -194,8 +201,10 @@ export default function BranchToolbar({...@@ -194,8 +201,10 @@ export default function BranchToolbar({
194 const name = rawName.trim();201 const name = rawName.trim();
195 const api = readNativeApi();202 const api = readNativeApi();
196 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;203 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;
204 setOptimisticBranch(name);
197 createBranchMutation.mutate(name, {205 createBranchMutation.mutate(name, {
198 onSuccess: () => {206 onSuccess: () => {
207 setOptimisticBranch(name);
199 setThreadError(null);208 setThreadError(null);
200 setThreadBranch(name, activeWorktreePath);209 setThreadBranch(name, activeWorktreePath);
201 setBranchQuery("");210 setBranchQuery("");
...@@ -203,6 +212,7 @@ export default function BranchToolbar({...@@ -203,6 +212,7 @@ export default function BranchToolbar({
203 onComposerFocusRequest?.();212 onComposerFocusRequest?.();
204 },213 },
205 onError: (error) => {214 onError: (error) => {
215 setOptimisticBranch(canonicalActiveBranch);
206 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");216 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
207 },217 },
208 });218 });
...@@ -223,9 +233,7 @@ export default function BranchToolbar({...@@ -223,9 +233,7 @@ export default function BranchToolbar({
223 variant="ghost"233 variant="ghost"
224 className="text-muted-foreground/70 hover:text-foreground/80"234 className="text-muted-foreground/70 hover:text-foreground/80"
225 size="xs"235 size="xs"
226 onClick={() =>236 onClick={() => onEnvModeChange(effectiveEnvMode === "local" ? "worktree" : "local")}
227 onEnvModeChange(effectiveEnvMode === "local" ? "worktree" : "local")
228 }
229 >237 >
230 {effectiveEnvMode === "worktree" ? "New worktree" : "Local"}238 {effectiveEnvMode === "worktree" ? "New worktree" : "Local"}
231 </Button>239 </Button>
...@@ -313,7 +321,9 @@ export default function BranchToolbar({...@@ -313,7 +321,9 @@ export default function BranchToolbar({
313 >321 >
314 <div className="flex w-full items-center justify-between gap-2">322 <div className="flex w-full items-center justify-between gap-2">
315 <span className="truncate">{itemValue}</span>323 <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 )}
317 </div>327 </div>
318 </ComboboxItem>328 </ComboboxItem>
319 );329 );