authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-27 23:13:11+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 23:13:11+02:00
loga2ffb122e2041731d1efe24d999aefe4e1ff7c03
tree33117a5c8289eadb8563b431e67f226f0fc2fca2
parent3957a958184650656dbbab6d73d9882de9855bd5
signature Signed by PGP key B5690EEEBB952194

Show origin ref in branch trigger label (#4680)

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

3 files changed, 150 insertions(+), 16 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+96
...@@ -9,6 +9,7 @@ import {...@@ -9,6 +9,7 @@ import {
9 resolveDraftEnvModeAfterBranchChange,9 resolveDraftEnvModeAfterBranchChange,
10 resolveEffectiveEnvMode,10 resolveEffectiveEnvMode,
11 resolveEnvModeLabel,11 resolveEnvModeLabel,
12 resolveBranchTriggerLabel,
12 resolveBranchToolbarPrBranch,13 resolveBranchToolbarPrBranch,
13 resolveBranchToolbarValue,14 resolveBranchToolbarValue,
14 resolveLockedWorkspaceLabel,15 resolveLockedWorkspaceLabel,
...@@ -174,6 +175,101 @@ describe("resolveBranchToolbarValue", () => {...@@ -174,6 +175,101 @@ describe("resolveBranchToolbarValue", () => {
174 });175 });
175});176});
176177
178describe("resolveBranchTriggerLabel", () => {
179 it("shows the origin ref when a new worktree will start from origin", () => {
180 expect(
181 resolveBranchTriggerLabel({
182 activeWorktreePath: null,
183 effectiveEnvMode: "worktree",
184 resolvedActiveBranch: "main",
185 resolvedActiveBranchIsRemote: false,
186 startFromOrigin: true,
187 }),
188 ).toBe("From origin/main");
189 });
190
191 it("shows the origin ref for local branch names that contain slashes", () => {
192 expect(
193 resolveBranchTriggerLabel({
194 activeWorktreePath: null,
195 effectiveEnvMode: "worktree",
196 resolvedActiveBranch: "feature/demo",
197 resolvedActiveBranchIsRemote: false,
198 startFromOrigin: true,
199 }),
200 ).toBe("From origin/feature/demo");
201 });
202
203 it("shows the local ref when start from origin is disabled", () => {
204 expect(
205 resolveBranchTriggerLabel({
206 activeWorktreePath: null,
207 effectiveEnvMode: "worktree",
208 resolvedActiveBranch: "main",
209 resolvedActiveBranchIsRemote: false,
210 startFromOrigin: false,
211 }),
212 ).toBe("From main");
213 });
214
215 it("does not duplicate the origin prefix for an explicit remote ref", () => {
216 expect(
217 resolveBranchTriggerLabel({
218 activeWorktreePath: null,
219 effectiveEnvMode: "worktree",
220 resolvedActiveBranch: "origin/feature/demo",
221 resolvedActiveBranchIsRemote: true,
222 startFromOrigin: true,
223 }),
224 ).toBe("From origin/feature/demo");
225 });
226
227 it("preserves an explicit ref from a non-origin remote", () => {
228 expect(
229 resolveBranchTriggerLabel({
230 activeWorktreePath: null,
231 effectiveEnvMode: "worktree",
232 resolvedActiveBranch: "upstream/feature/demo",
233 resolvedActiveBranchIsRemote: true,
234 startFromOrigin: true,
235 }),
236 ).toBe("From upstream/feature/demo");
237 });
238
239 it("keeps current-checkout labels and empty state unchanged", () => {
240 expect(
241 resolveBranchTriggerLabel({
242 activeWorktreePath: null,
243 effectiveEnvMode: "local",
244 resolvedActiveBranch: "main",
245 resolvedActiveBranchIsRemote: false,
246 startFromOrigin: true,
247 }),
248 ).toBe("main");
249 expect(
250 resolveBranchTriggerLabel({
251 activeWorktreePath: null,
252 effectiveEnvMode: "worktree",
253 resolvedActiveBranch: null,
254 resolvedActiveBranchIsRemote: null,
255 startFromOrigin: true,
256 }),
257 ).toBe("Select ref");
258 });
259
260 it("does not fabricate an origin ref while branch metadata is loading", () => {
261 expect(
262 resolveBranchTriggerLabel({
263 activeWorktreePath: null,
264 effectiveEnvMode: "worktree",
265 resolvedActiveBranch: "upstream/feature/demo",
266 resolvedActiveBranchIsRemote: null,
267 startFromOrigin: true,
268 }),
269 ).toBe("From upstream/feature/demo");
270 });
271});
272
177describe("resolveBranchToolbarPrBranch", () => {273describe("resolveBranchToolbarPrBranch", () => {
178 it("uses the explicit thread branch when it matches the displayed branch", () => {274 it("uses the explicit thread branch when it matches the displayed branch", () => {
179 expect(275 expect(
apps/web/src/components/BranchToolbar.logic.ts+27
...@@ -156,6 +156,33 @@ export function resolveBranchToolbarValue(input: {...@@ -156,6 +156,33 @@ export function resolveBranchToolbarValue(input: {
156 return currentGitBranch ?? activeThreadBranch;156 return currentGitBranch ?? activeThreadBranch;
157}157}
158158
159export function resolveBranchTriggerLabel(input: {
160 activeWorktreePath: string | null;
161 effectiveEnvMode: EnvMode;
162 resolvedActiveBranch: string | null;
163 resolvedActiveBranchIsRemote: boolean | null;
164 startFromOrigin: boolean;
165}): string {
166 const {
167 activeWorktreePath,
168 effectiveEnvMode,
169 resolvedActiveBranch,
170 resolvedActiveBranchIsRemote,
171 startFromOrigin,
172 } = input;
173 if (!resolvedActiveBranch) {
174 return "Select ref";
175 }
176 if (effectiveEnvMode === "worktree" && !activeWorktreePath) {
177 const baseRef =
178 startFromOrigin && resolvedActiveBranchIsRemote === false
179 ? `origin/${resolvedActiveBranch}`
180 : resolvedActiveBranch;
181 return `From ${baseRef}`;
182 }
183 return resolvedActiveBranch;
184}
185
159export function resolveBranchToolbarPrBranch(input: {186export function resolveBranchToolbarPrBranch(input: {
160 activeThreadBranch: string | null;187 activeThreadBranch: string | null;
161 resolvedActiveBranch: string | null;188 resolvedActiveBranch: string | null;
apps/web/src/components/BranchToolbarBranchSelector.tsx+27-16
...@@ -35,6 +35,7 @@ import { parsePullRequestReference } from "../pullRequestReference";...@@ -35,6 +35,7 @@ import { parsePullRequestReference } from "../pullRequestReference";
35import { getSourceControlPresentation } from "../sourceControlPresentation";35import { getSourceControlPresentation } from "../sourceControlPresentation";
36import {36import {
37 deriveLocalBranchNameFromRemoteRef,37 deriveLocalBranchNameFromRemoteRef,
38 resolveBranchTriggerLabel,
38 resolveBranchToolbarPrBranch,39 resolveBranchToolbarPrBranch,
39 resolveBranchSelectionTarget,40 resolveBranchSelectionTarget,
40 resolveBranchToolbarValue,41 resolveBranchToolbarValue,
...@@ -81,21 +82,6 @@ function toBranchActionErrorMessage(error: unknown): string {...@@ -81,21 +82,6 @@ function toBranchActionErrorMessage(error: unknown): string {
81 return error instanceof Error ? error.message : "An error occurred.";82 return error instanceof Error ? error.message : "An error occurred.";
82}83}
8384
84function getBranchTriggerLabel(input: {
85 activeWorktreePath: string | null;
86 effectiveEnvMode: "local" | "worktree";
87 resolvedActiveBranch: string | null;
88}): string {
89 const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch } = input;
90 if (!resolvedActiveBranch) {
91 return "Select ref";
92 }
93 if (effectiveEnvMode === "worktree" && !activeWorktreePath) {
94 return `From ${resolvedActiveBranch}`;
95 }
96 return resolvedActiveBranch;
97}
98
99export function BranchToolbarBranchSelector({85export function BranchToolbarBranchSelector({
100 className,86 className,
101 environmentId,87 environmentId,
...@@ -308,6 +294,29 @@ export function BranchToolbarBranchSelector({...@@ -308,6 +294,29 @@ export function BranchToolbarBranchSelector({
308 canonicalActiveBranch,294 canonicalActiveBranch,
309 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,295 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
310 );296 );
297 const listedActiveBranch =
298 resolvedActiveBranch === null ? null : (branchByName.get(resolvedActiveBranch) ?? null);
299 const activeBranchRefQuery = useEnvironmentQuery(
300 branchCwd !== null && resolvedActiveBranch !== null
301 ? vcsEnvironment.listRefs({
302 environmentId,
303 input: {
304 cwd: branchCwd,
305 query: resolvedActiveBranch,
306 limit: 10,
307 },
308 })
309 : null,
310 );
311 const queriedActiveBranch = activeBranchRefQuery.data?.refs.find(
312 (refName) => refName.name === resolvedActiveBranch,
313 );
314 const resolvedActiveBranchIsRemote =
315 listedActiveBranch !== null
316 ? listedActiveBranch.isRemote === true
317 : queriedActiveBranch
318 ? queriedActiveBranch.isRemote === true
319 : null;
311 const [isBranchActionPending, startBranchActionTransition] = useTransition();320 const [isBranchActionPending, startBranchActionTransition] = useTransition();
312 const totalBranchCount = branchRefState.data?.totalCount ?? 0;321 const totalBranchCount = branchRefState.data?.totalCount ?? 0;
313 const branchStatusText = isInitialBranchesLoadPending322 const branchStatusText = isInitialBranchesLoadPending
...@@ -584,10 +593,12 @@ export function BranchToolbarBranchSelector({...@@ -584,10 +593,12 @@ export function BranchToolbarBranchSelector({
584 maybeFetchNextBranchPage();593 maybeFetchNextBranchPage();
585 }, [refs.length, maybeFetchNextBranchPage]);594 }, [refs.length, maybeFetchNextBranchPage]);
586595
587 const triggerLabel = getBranchTriggerLabel({596 const triggerLabel = resolveBranchTriggerLabel({
588 activeWorktreePath,597 activeWorktreePath,
589 effectiveEnvMode,598 effectiveEnvMode,
590 resolvedActiveBranch,599 resolvedActiveBranch,
600 resolvedActiveBranchIsRemote,
601 startFromOrigin,
591 });602 });
592603
593 // PR pill shown next to the branch selector when the active branch has one.604 // PR pill shown next to the branch selector when the active branch has one.