From dda0d30afb4804101863f0bcac8b16e12e2ab299 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Mon, 17 Nov 2025 15:18:25 +0000
Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20default=20vs=20cus?=
=?UTF-8?q?tom=20trunk=20branch=20toggle?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Generated with
---
.../components/ChatInput/CreationControls.tsx | 55 ++++++++++++----
src/browser/components/ChatInput/index.tsx | 6 +-
.../ChatInput/useCreationWorkspace.ts | 9 ++-
.../hooks/useDraftWorkspaceSettings.ts | 63 ++++++++++++++++---
.../hooks/useStartWorkspaceCreation.test.ts | 4 ++
.../hooks/useStartWorkspaceCreation.ts | 8 ++-
src/common/constants/storage.ts | 8 +++
src/common/constants/workspace.ts | 12 ++++
8 files changed, 140 insertions(+), 25 deletions(-)
diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx
index dd231843b..fb219407d 100644
--- a/src/browser/components/ChatInput/CreationControls.tsx
+++ b/src/browser/components/ChatInput/CreationControls.tsx
@@ -2,11 +2,14 @@ import React from "react";
import { RUNTIME_MODE, type RuntimeMode } from "@/common/types/runtime";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { Select } from "../Select";
+import { DEFAULT_TRUNK_BRANCH, TRUNK_SELECTION, type TrunkSelection } from "@/common/constants/workspace";
interface CreationControlsProps {
branches: string[];
- trunkBranch: string;
- onTrunkBranchChange: (branch: string) => void;
+ trunkSelection: TrunkSelection;
+ customTrunkBranch: string;
+ onTrunkSelectionChange: (selection: TrunkSelection) => void;
+ onCustomTrunkBranchChange: (branch: string) => void;
runtimeMode: RuntimeMode;
sshHost: string;
onRuntimeChange: (mode: RuntimeMode, host: string) => void;
@@ -19,24 +22,52 @@ interface CreationControlsProps {
* - Runtime mode (local vs SSH)
*/
export function CreationControls(props: CreationControlsProps) {
+ const defaultUnavailable =
+ props.branches.length > 0 && !props.branches.includes(DEFAULT_TRUNK_BRANCH);
+ const showCustomPicker = props.trunkSelection === TRUNK_SELECTION.CUSTOM;
+
+ const handleTrunkSelectionChange = (value: string) => {
+ const selection = value as TrunkSelection;
+ if (selection === TRUNK_SELECTION.DEFAULT && defaultUnavailable) {
+ return;
+ }
+ props.onTrunkSelectionChange(selection);
+ };
+
return (
{/* Trunk Branch Selector */}
- {props.branches.length > 0 && (
-
-
+
+
+
+ {defaultUnavailable && (
+ Main branch not found
+ )}
+ {showCustomPicker && props.branches.length > 0 && (
-
- )}
+ )}
+
{/* Runtime Selector */}
diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx
index 20bbb1827..2cb337e74 100644
--- a/src/browser/components/ChatInput/index.tsx
+++ b/src/browser/components/ChatInput/index.tsx
@@ -1016,8 +1016,10 @@ export const ChatInput: React.FC = (props) => {
{variant === "creation" && (
void;
+ setTrunkSelection: (selection: TrunkSelection) => void;
runtimeMode: RuntimeMode;
sshHost: string;
setRuntimeOptions: (mode: RuntimeMode, host: string) => void;
@@ -62,7 +66,7 @@ export function useCreationWorkspace({
const [isSending, setIsSending] = useState(false);
// Centralized draft workspace settings with automatic persistence
- const { settings, setRuntimeOptions, setTrunkBranch, getRuntimeString } =
+ const { settings, setRuntimeOptions, setTrunkBranch, setTrunkSelection, getRuntimeString } =
useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);
// Get send options from shared hook (uses project-scoped storage key)
@@ -145,7 +149,10 @@ export function useCreationWorkspace({
return {
branches,
trunkBranch: settings.trunkBranch,
+ trunkSelection: settings.trunkSelection,
+ customTrunkBranch: settings.customTrunkBranch,
setTrunkBranch,
+ setTrunkSelection,
runtimeMode: settings.runtimeMode,
sshHost: settings.sshHost,
setRuntimeOptions,
diff --git a/src/browser/hooks/useDraftWorkspaceSettings.ts b/src/browser/hooks/useDraftWorkspaceSettings.ts
index 8908a7df4..77d528371 100644
--- a/src/browser/hooks/useDraftWorkspaceSettings.ts
+++ b/src/browser/hooks/useDraftWorkspaceSettings.ts
@@ -13,8 +13,10 @@ import {
getModelKey,
getRuntimeKey,
getTrunkBranchKey,
+ getTrunkSelectionKey,
getProjectScopeId,
} from "@/common/constants/storage";
+import { DEFAULT_TRUNK_BRANCH, TRUNK_SELECTION, type TrunkSelection } from "@/common/constants/workspace";
import type { UIMode } from "@/common/types/mode";
import type { ThinkingLevel } from "@/common/types/thinking";
@@ -33,6 +35,8 @@ export interface DraftWorkspaceSettings {
runtimeMode: RuntimeMode;
sshHost: string;
trunkBranch: string;
+ trunkSelection: TrunkSelection;
+ customTrunkBranch: string;
}
/**
@@ -52,6 +56,7 @@ export function useDraftWorkspaceSettings(
settings: DraftWorkspaceSettings;
setRuntimeOptions: (mode: RuntimeMode, host: string) => void;
setTrunkBranch: (branch: string) => void;
+ setTrunkSelection: (selection: TrunkSelection) => void;
getRuntimeString: () => string | undefined;
} {
// Global AI settings (read-only from global state)
@@ -75,22 +80,50 @@ export function useDraftWorkspaceSettings(
);
// Project-scoped trunk branch preference (persisted per project)
- const [trunkBranch, setTrunkBranch] = usePersistedState(
+ const [customTrunkBranch, setCustomTrunkBranch] = usePersistedState(
getTrunkBranchKey(projectPath),
"",
{ listener: true }
);
+ const [trunkSelection, setTrunkSelection] = usePersistedState(
+ getTrunkSelectionKey(projectPath),
+ TRUNK_SELECTION.DEFAULT,
+ { listener: true }
+ );
+
// Parse runtime string into mode and host
const { mode: runtimeMode, host: sshHost } = parseRuntimeModeAndHost(runtimeString);
- // Initialize trunk branch from backend recommendation or first branch
+ // Initialize custom trunk branch from backend recommendation or first branch
useEffect(() => {
- if (!trunkBranch && branches.length > 0) {
- const defaultBranch = recommendedTrunk ?? branches[0];
- setTrunkBranch(defaultBranch);
+ if (branches.length === 0) {
+ return;
}
- }, [branches, recommendedTrunk, trunkBranch, setTrunkBranch]);
+
+ const recommendedInList = recommendedTrunk && branches.includes(recommendedTrunk);
+ const currentIsValid = customTrunkBranch && branches.includes(customTrunkBranch);
+
+ if (currentIsValid) {
+ return;
+ }
+
+ const fallback = (recommendedInList ? recommendedTrunk : undefined) ?? branches[0];
+ setCustomTrunkBranch(fallback);
+ }, [branches, recommendedTrunk, customTrunkBranch, setCustomTrunkBranch]);
+
+ // Fall back to custom mode when default "main" is unavailable in the repo
+ useEffect(() => {
+ if (
+ branches.length === 0 ||
+ trunkSelection === TRUNK_SELECTION.CUSTOM ||
+ branches.includes(DEFAULT_TRUNK_BRANCH)
+ ) {
+ return;
+ }
+
+ setTrunkSelection(TRUNK_SELECTION.CUSTOM);
+ }, [branches, trunkSelection, setTrunkSelection]);
// Setter for runtime options (updates persisted runtime string)
const setRuntimeOptions = (newMode: RuntimeMode, newHost: string) => {
@@ -103,6 +136,17 @@ export function useDraftWorkspaceSettings(
return buildRuntimeString(runtimeMode, sshHost);
};
+ const resolvedCustomBranch =
+ customTrunkBranch ||
+ (recommendedTrunk ?? branches[0]) ||
+ DEFAULT_TRUNK_BRANCH;
+
+ const defaultAvailable = branches.length === 0 || branches.includes(DEFAULT_TRUNK_BRANCH);
+ const effectiveTrunkBranch =
+ trunkSelection === TRUNK_SELECTION.DEFAULT && defaultAvailable
+ ? DEFAULT_TRUNK_BRANCH
+ : resolvedCustomBranch;
+
return {
settings: {
model,
@@ -111,10 +155,13 @@ export function useDraftWorkspaceSettings(
use1M,
runtimeMode,
sshHost,
- trunkBranch,
+ trunkBranch: effectiveTrunkBranch,
+ trunkSelection,
+ customTrunkBranch: resolvedCustomBranch,
},
setRuntimeOptions,
- setTrunkBranch,
+ setTrunkBranch: setCustomTrunkBranch,
+ setTrunkSelection,
getRuntimeString,
};
}
diff --git a/src/browser/hooks/useStartWorkspaceCreation.test.ts b/src/browser/hooks/useStartWorkspaceCreation.test.ts
index d7bab6c67..41af1cb5d 100644
--- a/src/browser/hooks/useStartWorkspaceCreation.test.ts
+++ b/src/browser/hooks/useStartWorkspaceCreation.test.ts
@@ -12,8 +12,10 @@ import {
getProjectScopeId,
getRuntimeKey,
getTrunkBranchKey,
+ getTrunkSelectionKey,
} from "@/common/constants/storage";
import type { ProjectConfig } from "@/node/config";
+import { TRUNK_SELECTION } from "@/common/constants/workspace";
import type { updatePersistedState } from "@/browser/hooks/usePersistedState";
@@ -71,6 +73,7 @@ describe("persistWorkspaceCreationPrefill", () => {
expect(callMap.get(getInputKey(getPendingScopeId(projectPath)))).toBe("Ship it");
expect(callMap.get(getModelKey(getProjectScopeId(projectPath)))).toBe("provider/model");
expect(callMap.get(getTrunkBranchKey(projectPath))).toBe("main");
+ expect(callMap.get(getTrunkSelectionKey(projectPath))).toBe(TRUNK_SELECTION.CUSTOM);
expect(callMap.get(getRuntimeKey(projectPath))).toBe("ssh dev");
});
@@ -90,6 +93,7 @@ describe("persistWorkspaceCreationPrefill", () => {
}
expect(callMap.get(getTrunkBranchKey(projectPath))).toBeUndefined();
+ expect(callMap.get(getTrunkSelectionKey(projectPath))).toBe(TRUNK_SELECTION.DEFAULT);
expect(callMap.get(getRuntimeKey(projectPath))).toBeUndefined();
});
diff --git a/src/browser/hooks/useStartWorkspaceCreation.ts b/src/browser/hooks/useStartWorkspaceCreation.ts
index 10d55de5d..3cf9a744d 100644
--- a/src/browser/hooks/useStartWorkspaceCreation.ts
+++ b/src/browser/hooks/useStartWorkspaceCreation.ts
@@ -10,8 +10,10 @@ import {
getProjectScopeId,
getRuntimeKey,
getTrunkBranchKey,
+ getTrunkSelectionKey,
} from "@/common/constants/storage";
import { RUNTIME_MODE, SSH_RUNTIME_PREFIX } from "@/common/types/runtime";
+import { TRUNK_SELECTION } from "@/common/constants/workspace";
export type StartWorkspaceCreationDetail =
CustomEventPayloads[typeof CUSTOM_EVENTS.START_WORKSPACE_CREATION];
@@ -69,9 +71,11 @@ export function persistWorkspaceCreationPrefill(
if (detail.trunkBranch !== undefined) {
const normalizedTrunk = detail.trunkBranch.trim();
+ const hasCustomTrunk = normalizedTrunk.length > 0;
+ persist(getTrunkBranchKey(projectPath), hasCustomTrunk ? normalizedTrunk : undefined);
persist(
- getTrunkBranchKey(projectPath),
- normalizedTrunk.length > 0 ? normalizedTrunk : undefined
+ getTrunkSelectionKey(projectPath),
+ hasCustomTrunk ? TRUNK_SELECTION.CUSTOM : TRUNK_SELECTION.DEFAULT
);
}
diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts
index 5a2b2f121..6bded53ba 100644
--- a/src/common/constants/storage.ts
+++ b/src/common/constants/storage.ts
@@ -108,6 +108,14 @@ export function getTrunkBranchKey(projectPath: string): string {
return `trunkBranch:${projectPath}`;
}
+/**
+ * Get the localStorage key for trunk branch selection mode (default vs custom)
+ * Format: "trunkSelection:{projectPath}"
+ */
+export function getTrunkSelectionKey(projectPath: string): string {
+ return `trunkSelection:${projectPath}`;
+}
+
/**
* Get the localStorage key for the 1M context preference (global)
* Format: "use1MContext"
diff --git a/src/common/constants/workspace.ts b/src/common/constants/workspace.ts
index 04f6e3e34..f3aaa5d19 100644
--- a/src/common/constants/workspace.ts
+++ b/src/common/constants/workspace.ts
@@ -8,3 +8,15 @@ export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
type: "local",
srcBaseDir: "~/.mux/src",
} as const;
+
+/**
+ * Default trunk branch to fork from when creating workspaces
+ */
+export const DEFAULT_TRUNK_BRANCH = "main" as const;
+
+export const TRUNK_SELECTION = {
+ DEFAULT: "default",
+ CUSTOM: "custom",
+} as const;
+
+export type TrunkSelection = (typeof TRUNK_SELECTION)[keyof typeof TRUNK_SELECTION];