authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 21:35:42-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 22:36:35-08:00
log69a4fa82155f05637f73b3d2f61a5cdf9366b4b9
treefcd87a8439e1006d85ad0f18a9ff7eef2a190e97
parent9e04bad0a504dafbf19182fb0ec361c55018c780

Show branch toolbar for local draft threads

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 48 insertions(+), 19 deletions(-)

apps/web/src/components/BranchToolbar.tsx+48-19
...@@ -9,6 +9,7 @@ import {...@@ -9,6 +9,7 @@ import {
9 gitCreateBranchAndCheckoutMutationOptions,9 gitCreateBranchAndCheckoutMutationOptions,
10} from "../lib/gitReactQuery";10} from "../lib/gitReactQuery";
11import { readNativeApi } from "../nativeApi";11import { readNativeApi } from "../nativeApi";
12import { useComposerDraftStore } from "../composerDraftStore";
12import { useStore } from "../store";13import { useStore } from "../store";
13import {14import {
14 dedupeRemoteBranchesWithLocalMatches,15 dedupeRemoteBranchesWithLocalMatches,
...@@ -43,17 +44,21 @@ export default function BranchToolbar({...@@ -43,17 +44,21 @@ export default function BranchToolbar({
43 onComposerFocusRequest,44 onComposerFocusRequest,
44}: BranchToolbarProps) {45}: BranchToolbarProps) {
45 const { state, dispatch } = useStore();46 const { state, dispatch } = useStore();
47 const draftThread = useComposerDraftStore((store) => store.getDraftThread(threadId));
48 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
46 const queryClient = useQueryClient();49 const queryClient = useQueryClient();
4750
48 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);51 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);
49 const [branchQuery, setBranchQuery] = useState("");52 const [branchQuery, setBranchQuery] = useState("");
5053
51 const activeThread = state.threads.find((thread) => thread.id === threadId);54 const serverThread = state.threads.find((thread) => thread.id === threadId);
52 const activeProject = state.projects.find((project) => project.id === activeThread?.projectId);55 const activeProjectId = serverThread?.projectId ?? draftThread?.projectId ?? null;
53 const activeThreadId = activeThread?.id;56 const activeProject = state.projects.find((project) => project.id === activeProjectId);
54 const activeThreadBranch = activeThread?.branch ?? null;57 const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined);
55 const activeWorktreePath = activeThread?.worktreePath ?? null;58 const activeThreadBranch = serverThread?.branch ?? draftThread?.branch ?? null;
59 const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null;
56 const branchCwd = activeWorktreePath ?? activeProject?.cwd ?? null;60 const branchCwd = activeWorktreePath ?? activeProject?.cwd ?? null;
61 const hasServerThread = serverThread !== undefined;
5762
58 // ── Queries ───────────────────────────────────────────────────────────63 // ── Queries ───────────────────────────────────────────────────────────
5964
...@@ -95,7 +100,7 @@ export default function BranchToolbar({...@@ -95,7 +100,7 @@ export default function BranchToolbar({
95 if (!activeThreadId || !syncedBranch) return;100 if (!activeThreadId || !syncedBranch) return;
96 const api = readNativeApi();101 const api = readNativeApi();
97102
98 if (api) {103 if (api && hasServerThread) {
99 void api.orchestration.dispatchCommand({104 void api.orchestration.dispatchCommand({
100 type: "thread.meta.update",105 type: "thread.meta.update",
101 commandId: newCommandId(),106 commandId: newCommandId(),
...@@ -104,13 +109,30 @@ export default function BranchToolbar({...@@ -104,13 +109,30 @@ export default function BranchToolbar({
104 worktreePath: null,109 worktreePath: null,
105 });110 });
106 }111 }
107 dispatch({112 if (hasServerThread) {
108 type: "SET_THREAD_BRANCH",113 dispatch({
109 threadId: activeThreadId,114 type: "SET_THREAD_BRANCH",
115 threadId: activeThreadId,
116 branch: syncedBranch,
117 worktreePath: null,
118 });
119 return;
120 }
121 setDraftThreadContext(threadId, {
110 branch: syncedBranch,122 branch: syncedBranch,
111 worktreePath: null,123 worktreePath: null,
112 });124 });
113 }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, envMode, dispatch]);125 }, [
126 activeThreadId,
127 activeWorktreePath,
128 activeThreadBranch,
129 queryBranches,
130 envMode,
131 dispatch,
132 hasServerThread,
133 setDraftThreadContext,
134 threadId,
135 ]);
114136
115 useEffect(() => {137 useEffect(() => {
116 if (isBranchMenuOpen) return;138 if (isBranchMenuOpen) return;
...@@ -129,7 +151,7 @@ export default function BranchToolbar({...@@ -129,7 +151,7 @@ export default function BranchToolbar({
129 const api = readNativeApi();151 const api = readNativeApi();
130 // If the effective cwd is about to change, stop the running session so the152 // If the effective cwd is about to change, stop the running session so the
131 // next message creates a new one with the correct cwd.153 // next message creates a new one with the correct cwd.
132 const sessionId = activeThread?.session?.sessionId;154 const sessionId = serverThread?.session?.sessionId;
133 if (sessionId && worktreePath !== activeWorktreePath && api) {155 if (sessionId && worktreePath !== activeWorktreePath && api) {
134 void api.orchestration156 void api.orchestration
135 .dispatchCommand({157 .dispatchCommand({
...@@ -140,7 +162,7 @@ export default function BranchToolbar({...@@ -140,7 +162,7 @@ export default function BranchToolbar({
140 })162 })
141 .catch(() => undefined);163 .catch(() => undefined);
142 }164 }
143 if (api) {165 if (api && hasServerThread) {
144 void api.orchestration.dispatchCommand({166 void api.orchestration.dispatchCommand({
145 type: "thread.meta.update",167 type: "thread.meta.update",
146 commandId: newCommandId(),168 commandId: newCommandId(),
...@@ -149,7 +171,14 @@ export default function BranchToolbar({...@@ -149,7 +171,14 @@ export default function BranchToolbar({
149 worktreePath,171 worktreePath,
150 });172 });
151 }173 }
152 dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId, branch, worktreePath });174 if (hasServerThread) {
175 dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId, branch, worktreePath });
176 return;
177 }
178 setDraftThreadContext(threadId, {
179 branch,
180 worktreePath,
181 });
153 };182 };
154183
155 const selectBranch = (branch: GitBranch) => {184 const selectBranch = (branch: GitBranch) => {
...@@ -221,7 +250,7 @@ export default function BranchToolbar({...@@ -221,7 +250,7 @@ export default function BranchToolbar({
221 });250 });
222 };251 };
223252
224 if (!activeThread || !activeProject) return null;253 if (!activeThreadId || !activeProject) return null;
225254
226 return (255 return (
227 <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1">256 <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1">
...@@ -248,7 +277,7 @@ export default function BranchToolbar({...@@ -248,7 +277,7 @@ export default function BranchToolbar({
248 autoHighlight277 autoHighlight
249 onOpenChange={(open) => setIsBranchMenuOpen(open)}278 onOpenChange={(open) => setIsBranchMenuOpen(open)}
250 open={isBranchMenuOpen}279 open={isBranchMenuOpen}
251 value={activeThread.branch}280 value={activeThreadBranch}
252 >281 >
253 <ComboboxTrigger282 <ComboboxTrigger
254 render={<Button variant="ghost" size="xs" />}283 render={<Button variant="ghost" size="xs" />}
...@@ -256,10 +285,10 @@ export default function BranchToolbar({...@@ -256,10 +285,10 @@ export default function BranchToolbar({
256 disabled={branchesQuery.isLoading}285 disabled={branchesQuery.isLoading}
257 >286 >
258 <span className="max-w-[240px] truncate">287 <span className="max-w-[240px] truncate">
259 {activeThread.branch288 {activeThreadBranch
260 ? envMode === "worktree" && !activeWorktreePath289 ? envMode === "worktree" && !activeWorktreePath
261 ? `From ${activeThread.branch}`290 ? `From ${activeThreadBranch}`
262 : activeThread.branch291 : activeThreadBranch
263 : "Select branch"}292 : "Select branch"}
264 </span>293 </span>
265 <ChevronDownIcon />294 <ChevronDownIcon />
...@@ -313,7 +342,7 @@ export default function BranchToolbar({...@@ -313,7 +342,7 @@ export default function BranchToolbar({
313 key={itemValue}342 key={itemValue}
314 value={itemValue}343 value={itemValue}
315 className={344 className={
316 itemValue === activeThread.branch ? "bg-accent text-foreground" : undefined345 itemValue === activeThreadBranch ? "bg-accent text-foreground" : undefined
317 }346 }
318 onClick={() => selectBranch(branch)}347 onClick={() => selectBranch(branch)}
319 >348 >