Repository navigation
feat(settings): choose the branch new worktrees start from #22750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import type { ServerSettingsPatch } from "@t3tools/contracts"; | ||
| import { useEffect, useEffectEvent, useRef } from "react"; | ||
| import { View } from "react-native"; | ||
|
|
||
| import { AppText as Text, AppTextInput } from "../../../components/ui/AppText"; | ||
|
|
||
| /** Base branch for new worktrees. `null` means the selected targets differ. */ | ||
| export function WorktreeBaseBranchField(props: { | ||
| value: string | null; | ||
| disabled: boolean; | ||
| onChange: (patch: ServerSettingsPatch) => void; | ||
| }) { | ||
| // Typed text waits here until it saves. Android's Back closes the keyboard | ||
| // without ending editing, so leaving the screen saves it too. | ||
| const draft = useRef<{ text: string; over: string | null } | null>(null); | ||
| // A newer saved value remounts the input, so a draft typed over an older | ||
| // one is no longer on screen and is dropped. | ||
| const save = () => { | ||
| const current = draft.current; | ||
| draft.current = null; | ||
| if (current === null || current.over !== props.value) return; | ||
| const value = current.text.trim(); | ||
| if (props.value === null || value !== props.value) { | ||
| props.onChange({ newWorktreesBaseBranch: value }); | ||
| } | ||
| }; | ||
| const saveOnLeave = useEffectEvent(save); | ||
| useEffect(() => () => saveOnLeave(), []); | ||
| return ( | ||
| <View className="gap-2 px-4 py-3"> | ||
| <Text className="text-sm text-foreground-muted"> | ||
| New worktrees start from this branch. Leave empty to use the default branch. | ||
| </Text> | ||
| <AppTextInput | ||
| key={props.value} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: cat apps/mobile/src/features/settings/components/WorktreeBaseBranchField.tsx
sed -n '380,410p' apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
rg -n 'ServerSettingsDetail' apps/mobile/src | headRepository: pingdotgg/t3code Length of output: 4440 🏁 Script executed: sed -n '90,190p' apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
sed -n '190,460p' apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx
rg -n -F --glob '*.tsx' -e 'SettingsServerControlsRouteScreen' -e 'ServerSettingsDetail' -e 'targetsKey' -e 'target' apps/mobile/src/features/settings apps/mobile/srcRepository: pingdotgg/t3code Length of output: 41853 Bind the field lifetime to
🐛 Suggested fix <WorktreeBaseBranchField
+ key={targetsKey}
value={uniform("newWorktreesBaseBranch")}
disabled={disabledFor("newWorktreesBaseBranch")}
onChange={write}
/>🤖 Prompt for AI Agents |
||
| accessibilityLabel="Worktree base branch" | ||
| onChangeText={(text) => { | ||
| draft.current = { text, over: props.value }; | ||
| }} | ||
| defaultValue={props.value ?? ""} | ||
| placeholder={props.value === null ? "Mixed" : "Default branch"} | ||
| editable={!props.disabled} | ||
| autoCapitalize="none" | ||
| autoCorrect={false} | ||
| className="min-h-10 rounded-xl px-3 py-2 text-base text-foreground" | ||
| onEndEditing={save} | ||
| /> | ||
| </View> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -892,9 +892,14 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | |
| if (live && (live.mode !== "worktree" || live.branch !== null)) { | ||
| return; | ||
| } | ||
| // The default may only exist as origin/<default> (isRemote), which | ||
| // availableBranches filters out — search the unfiltered refs for it. | ||
| // The project's configured base branch wins. The default may only exist | ||
| // as origin/<default> (isRemote), which availableBranches filters out — | ||
| // search the unfiltered refs for it. | ||
| const configuredBaseBranch = projectSettings.settings.newWorktreesBaseBranch; | ||
| const preferredBranch = | ||
| (configuredBaseBranch.length > 0 | ||
| ? allBranchRefs.find((branch) => branch.name === configuredBaseBranch) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High A configured base branch outside the currently loaded page is skipped, so this effect persists the default or current branch instead and new worktrees start from the wrong branch. Because 🤖 Copy this AI Prompt to have your agent fix this: |
||
| : undefined) ?? | ||
|
Comment on lines
+900
to
+902
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '860,925p' apps/mobile/src/features/threads/new-task-flow-provider.tsx
rg -n 'allBranchRefs' apps/mobile/src | head -30Repository: pingdotgg/t3code Length of output: 2985 🏁 Script executed: set -o pipefail
rg -n -F --glob '*.{ts,tsx}' -e 'branchState' -e 'loadNext' -e 'isRemote' -e 'BranchRef' -e 'refs:' apps/mobile packages apps/web | head -300
printf '\n--- provider setup ---\n'
sed -n '640,705p' apps/mobile/src/features/threads/new-task-flow-provider.tsxRepository: pingdotgg/t3code Length of output: 19324 🏁 Script executed: printf '%s\n' '--- paginated branch hook locations ---'
rg -n -F --glob '*.{ts,tsx}' -e 'usePaginatedBranches' -e 'paginatedBranches' apps/mobile/src packages apps/server | head -120
printf '%s\n' '--- pagination implementation candidates ---'
sed -n '1,360p' apps/mobile/src/state/paginatedBranches.ts 2>/dev/null || true
sed -n '210,315p' apps/mobile/src/state/queries.ts
printf '%s\n' '--- VCS ref contract ---'
sed -n '60,100p' packages/contracts/src/git.ts
sed -n '250,295p' packages/contracts/src/git.tsRepository: pingdotgg/t3code Length of output: 7454 🏁 Script executed: set -o pipefail
rg -n -F --glob '*.{ts,tsx}' -e 'listRefs' -e 'remoteName:' -e 'upstreamRef:' -e 'refs/remotes/' packages apps/server apps/mobile | head -300Repository: pingdotgg/t3code Length of output: 32582 🏁 Script executed: sed -n '3380,3510p' apps/server/src/vcs/GitVcsDriverCore.ts; sed -n '3610,3735p' apps/server/src/vcs/GitVcsDriverCore.tsRepository: pingdotgg/t3code Length of output: 10668 🏁 Script executed: nl -ba apps/mobile/src/state/queries.ts | sed -n '205,305p'
nl -ba apps/mobile/src/features/threads/new-task-flow-provider.tsx | sed -n '885,915p'
nl -ba apps/server/src/vcs/GitVcsDriverCore.ts | sed -n '3478,3505p'
rg -n -F --glob '*.ts' -e 'VCS_REF_LIST_LIMIT' -e 'function paginateBranches' apps/mobile/src apps/server/srcRepository: pingdotgg/t3code Length of output: 7199 Resolve the configured branch before selecting a fallback. If Query the configured ref independently of the paginated picker results. Support both the configured short name and its remote-only form before applying the fallback. 🤖 Prompt for AI Agents |
||
| allBranchRefs.find((branch) => branch.isDefault) ?? | ||
| availableBranches.find((branch) => branch.current) ?? | ||
| null; | ||
|
|
@@ -905,6 +910,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | |
| allBranchRefs, | ||
| availableBranches, | ||
| defaultWorkspaceModeSettled, | ||
| projectSettings.settings.newWorktreesBaseBranch, | ||
| selectBranch, | ||
| selectedBranchName, | ||
| selectedProjectDraftKey, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ import { readLocalApi } from "../localApi"; | |
| import { useOpenPrLink } from "../lib/openPullRequestLink"; | ||
| import { usePaginatedBranches } from "../state/queries"; | ||
| import { useProject, useQuietThreadShell } from "../state/entities"; | ||
| import { useEnvironmentSettings } from "../hooks/useSettings"; | ||
| import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; | ||
| import { useEnvironmentQuery } from "../state/query"; | ||
| import { readEnvironmentScope, useEnvironmentScope } from "~/state/session"; | ||
| import { threadEnvironment } from "../state/threads"; | ||
|
|
@@ -53,6 +55,7 @@ import { | |
| resolveBranchToolbarValue, | ||
| resolveDraftEnvModeAfterBranchChange, | ||
| resolveEffectiveEnvMode, | ||
| resolveWorktreeBaseBranch, | ||
| sanitizeNewRefName, | ||
| shouldIncludeBranchPickerItem, | ||
| } from "./BranchToolbar.logic"; | ||
|
|
@@ -143,6 +146,7 @@ export function BranchToolbarBranchSelector({ | |
| ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) | ||
| : null; | ||
| const activeProject = useProject(activeProjectRef); | ||
| const settings = useEnvironmentSettings(environmentId); | ||
|
|
||
| const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined); | ||
| const activeThreadBranch = | ||
|
|
@@ -477,9 +481,18 @@ export function BranchToolbarBranchSelector({ | |
| }); | ||
| }; | ||
|
|
||
| const configuredBaseBranch = resolveProjectSettings( | ||
| settings, | ||
| activeProject?.id ?? null, | ||
| activeProject ?? undefined, | ||
| ).settings.newWorktreesBaseBranch; | ||
|
Comment on lines
+484
to
+488
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: set -e
printf '%s\n' '--- diff ---'
git diff --unified=80 618761a2d6d615b2c0a07e5de4d82384a583f717 56613102538927bf6cd2e484552675fa73eaa404 -- apps/web/src/components/BranchToolbarBranchSelector.tsx
printf '%s\n' '--- selector references and imports ---'
rg -n -F -- 'useEnvironmentSettings' apps/web/src packages || true
rg -n -F -- 'BranchToolbarBranchSelector' apps/web/src || true
printf '%s\n' '--- selector outline ---'
ast-grep outline apps/web/src/components/BranchToolbarBranchSelector.tsx --view expanded
printf '%s\n' '--- relevant selector blocks ---'
rg -n -C 35 -F -e 'resolveProjectSettings' -e 'activeThreadBranch' -e 'defaultBranchName' -e 'useEnvironmentSettings' apps/web/src/components/BranchToolbarBranchSelector.tsx
printf '%s\n' '--- hook definitions ---'
rg -n -F -- 'function useEnvironmentSettings' apps/web/src packages || true
rg -n -F -- 'const useEnvironmentSettings' apps/web/src packages || true
rg -n -F -- 'useEnvironmentSettings =' apps/web/src packages || true
printf '%s\n' '--- settings resolver ---'
rg -n -F -- 'function resolveProjectSettings' apps packages || true
rg -n -F -- 'resolveProjectSettings =' apps packages || true
printf '%s\n' '--- branch loading symbols ---'
rg -n -F -e 'branchRefs' -e 'branchRef' -e 'defaultBranchName' apps/web/src/components apps/web/src/hooks packages/shared | head -240Repository: pingdotgg/t3code Length of output: 39661 🏁 Script executed: set -e
printf '%s\n' '--- useEnvironmentSettings implementation ---'
sed -n '470,570p' apps/web/src/hooks/useSettings.ts
printf '%s\n' '--- settings hook imports and query sources ---'
sed -n '1,120p' apps/web/src/hooks/useSettings.ts
rg -n -C 18 -F -e 'environmentSettings' -e 'serverSettings' -e 'settingsQuery' apps/web/src/hooks/useSettings.ts apps/web/src/state packages/client-runtime packages/shared || true
printf '%s\n' '--- usePaginatedBranches implementation and imports ---'
rg -n -F -- 'function usePaginatedBranches' apps/web/src
rg -n -F -- 'usePaginatedBranches =' apps/web/src
sed -n '1,180p' apps/web/src/state/queries.ts
printf '%s\n' '--- draft creation and worktree mode callers ---'
rg -n -C 25 -F -e 'forceNewWorktree' -e 'effectiveEnvModeOverride' -e 'onActiveThreadBranchOverrideChange' apps/web/src/components/BranchToolbar.tsx apps/web/src/components apps/web/src | head -520
printf '%s\n' '--- settings atom/query definitions ---'
rg -n -F -e 'readEnvironmentSettings' -e 'environmentSettings' -e 'serverSettings' -e 'settingsAtom' apps/web/src/state apps/web/src/hooks packages/client-runtime packages/shared | head -300Repository: pingdotgg/t3code Length of output: 36354 🏁 Script executed: set +e
printf '%s\n' '--- paginated branch query ---'
sed -n '90,165p' apps/web/src/state/queries.ts
printf '%s\n' '--- settings atom definitions ---'
rg -n -C 22 -F -e 'settingsValueAtom' -e 'settingsValue' apps/web/src/state/server.ts apps/web/src/state apps/web/src | head -260
printf '%s\n' '--- branch query primitives ---'
rg -n -C 20 -F -e 'usePaginatedQuery' -e 'isPending' apps/web/src/state/queries.ts apps/web/src/state/query.ts apps/web/src/state | head -300
printf '%s\n' '--- BranchToolbar callers complete blocks ---'
sed -n '600,880p' apps/web/src/components/BranchToolbar.tsx
printf '%s\n' '--- new draft worktree mode references ---'
rg -n -C 30 -F -e 'envMode: "worktree"' -e 'newWorktree' -e 'forceNewWorktree' apps/web/src/components apps/web/src/state | head -500Repository: pingdotgg/t3code Length of output: 42181 🏁 Script executed: set +e
printf '%s\n' '--- complete branch query return ---'
sed -n '120,225p' apps/web/src/state/queries.ts
printf '%s\n' '--- server state definition ---'
rg -n --glob '*.ts' --glob '*.tsx' -F -e 'settingsValueAtom' apps packages | head -80
printf '%s\n' '--- selector exact causal block ---'
nl -ba apps/web/src/components/BranchToolbarBranchSelector.tsx | sed -n '143,170p;223,285p;484,520p'
printf '%s\n' '--- draft setter definition ---'
rg -n -F -- 'setDraftThreadContext' apps/web/src/components/composerDraftStore.ts apps/web/src | head -40Repository: pingdotgg/t3code Length of output: 15567 🏁 Script executed: set -e
printf '%s\n' '--- worktree resolver ---'
rg -n -C 18 -F -- 'function resolveWorktreeBaseBranch' apps/web/src/components/BranchToolbar.logic.ts
printf '%s\n' '--- setting default ---'
rg -n -C 8 -F -- 'newWorktreesBaseBranch' packages/contracts/srcRepository: pingdotgg/t3code Length of output: 7709 Wait for saved settings before selecting the worktree base branch. When the settings atom is still Gate automatic selection on the raw settings snapshot: Suggested fix+import { useAtomValue } from "@effect/atom-react";
import { useEnvironmentSettings } from "../hooks/useSettings";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
+import { serverEnvironment } from "../state/server";
...
const activeProject = useProject(activeProjectRef);
+ const serverSettings = useAtomValue(serverEnvironment.settingsValueAtom(environmentId));
const settings = useEnvironmentSettings(environmentId);
...
- const worktreeBaseBranchCandidate = isInitialBranchesLoadPending
+ const worktreeBaseBranchCandidate = isInitialBranchesLoadPending || serverSettings === null
? null
: resolveWorktreeBaseBranch({🤖 Prompt for AI Agents |
||
| const worktreeBaseBranchCandidate = isInitialBranchesLoadPending | ||
| ? null | ||
| : (defaultBranchName ?? currentGitBranch); | ||
| : resolveWorktreeBaseBranch({ | ||
| configuredBaseBranch, | ||
| defaultBranch: defaultBranchName, | ||
| currentGitBranch, | ||
| }); | ||
|
|
||
| // A started thread's server is creating its worktree and records the branch | ||
| // itself; defaulting it here would reset the thread to the base checkout and | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { useRef } from "react"; | ||
| import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts"; | ||
|
|
||
| import { Input } from "../ui/input"; | ||
| import { SettingsRow, SettingResetButton } from "./settingsLayout"; | ||
| import { useSettingsScope } from "./SettingsScopeContext"; | ||
| import { searchableSetting } from "./settingsSearch"; | ||
| import { | ||
| useScopedSettings, | ||
| useScopedSettingsMixed, | ||
| useUpdateScopedSettings, | ||
| } from "./useScopedSettings"; | ||
|
|
||
| export function WorktreeBaseBranchSetting() { | ||
| const settings = useScopedSettings(); | ||
| const { targets } = useSettingsScope(); | ||
| const scopeKey = targets.map((target) => `${target.environmentId}:${target.projectId}`).join(","); | ||
| const edited = useRef(false); | ||
| const updateSettings = useUpdateScopedSettings(); | ||
| const mixed = useScopedSettingsMixed(["newWorktreesBaseBranch"]); | ||
|
|
||
| return ( | ||
| <SettingsRow | ||
| serverScoped | ||
| settingKeys={["newWorktreesBaseBranch"]} | ||
| {...searchableSetting("worktree-base-branch")} | ||
| description="New worktrees start from this branch. Leave empty to use the repository default branch." | ||
| resetAction={ | ||
| mixed || | ||
| settings.newWorktreesBaseBranch !== DEFAULT_SERVER_SETTINGS.newWorktreesBaseBranch ? ( | ||
| <SettingResetButton | ||
| label="worktree base branch" | ||
| onClick={() => | ||
| updateSettings({ | ||
| newWorktreesBaseBranch: DEFAULT_SERVER_SETTINGS.newWorktreesBaseBranch, | ||
| }) | ||
| } | ||
| /> | ||
| ) : null | ||
| } | ||
| control={ | ||
| <Input | ||
| key={`${scopeKey}:${mixed}:${settings.newWorktreesBaseBranch}`} | ||
| aria-label="Worktree base branch" | ||
| autoCapitalize="none" | ||
| spellCheck={false} | ||
| onChange={() => { | ||
| edited.current = true; | ||
| }} | ||
| placeholder={mixed ? "Mixed" : "Default branch"} | ||
| defaultValue={mixed ? "" : settings.newWorktreesBaseBranch} | ||
| onBlur={(event) => { | ||
| const value = event.target.value.trim(); | ||
| if (edited.current && (mixed || value !== settings.newWorktreesBaseBranch)) | ||
| updateSettings({ newWorktreesBaseBranch: value }); | ||
| edited.current = false; | ||
| }} | ||
| /> | ||
| } | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
components/WorktreeBaseBranchField.tsx:21A draft typed for one project/environment is saved to the newly selected project/environment when both scopes have the same
value, so the edit changes the wrong settings. The draft guard compares onlyprops.value, and the retained field receives the new scope’sonChange; bind the draft to the target identity or remount the field whentargetsKeychanges.Also found in 1 other location(s)
apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx:398🤖 Copy this AI Prompt to have your agent fix this: