fix(worktrees): a paired host honors the client's Keep Local Main Up to Date (#29469)

The desktop keeps the toggle and its dismissed-suggestion flag in its own
profile, but the host read its own copy on create, so local main never
fast-forwarded and the suggestion toast kept returning. Send both with
worktree.create, like nestWorkspaces; older clients keep the host's value.

Fixes STA-6997
This commit is contained in:
Neil
2026-10-15 23:28:54 -07:00
committed by GitHub
parent eccbd17bf0
commit 2b140a4d9b
8 changed files with 92 additions and 6 deletions
@@ -48,6 +48,12 @@ export function buildManagedWorktreeCreateArgs(
workspaceStatus: params.workspaceStatus,
manualOrder: params.manualOrder,
...(params.nestWorkspaces !== undefined ? { nestWorkspaces: params.nestWorkspaces } : {}),
...(params.refreshLocalBaseRefOnWorktreeCreate !== undefined
? { refreshLocalBaseRefOnWorktreeCreate: params.refreshLocalBaseRefOnWorktreeCreate }
: {}),
...(params.localBaseRefSuggestionDismissed !== undefined
? { localBaseRefSuggestionDismissed: params.localBaseRefSuggestionDismissed }
: {}),
sparseCheckout: params.sparseCheckout,
pushTarget: params.pushTarget,
runHooks: params.runHooks === true,
@@ -200,6 +200,42 @@ describe('runtime nested-workspaces layout', () => {
})
})
describe('runtime keep-local-main-up-to-date choice', () => {
const addRefreshArg = (): unknown => mocks.add.mock.calls[0]?.[4]
const addOptions = (): AddWorktreeOptions | undefined => mocks.add.mock.calls[0]?.[6]
beforeEach(() => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'disabled', rearm: mocks.rearm })
mocks.remoteBase.mockResolvedValue({
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
})
})
it('refreshes local main when the client turned it on, over the host default', async () => {
await createWorktree({ refreshLocalBaseRefOnWorktreeCreate: true })
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({ refreshLocalBaseRef: true })
)
expect(addRefreshArg()).toBe(true)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBeUndefined()
})
it('suppresses the suggestion the client already dismissed', async () => {
await createWorktree({ localBaseRefSuggestionDismissed: true })
expect(addRefreshArg()).toBe(false)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBeUndefined()
})
it('keeps the host setting and suggestion when the client sends neither', async () => {
await createWorktree({})
expect(addRefreshArg()).toBe(false)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBe(true)
})
})
describe('runtime prepared-worktree replenishment', () => {
it('keeps a failed claim reserved through the normal-add fallback', async () => {
mocks.consume.mockResolvedValue({
@@ -70,7 +70,13 @@ async function performRuntimeLocalWorktreeCreate<T>(args: RuntimeLocalWorktreeCr
// Why: the layout choice must reach name retirement and metadata too, not just the path.
const settings = {
...hostSettings,
nestWorkspaces: request.nestWorkspaces ?? hostSettings.nestWorkspaces
nestWorkspaces: request.nestWorkspaces ?? hostSettings.nestWorkspaces,
// Why: a paired desktop keeps this toggle in its own profile, never in the host's.
refreshLocalBaseRefOnWorktreeCreate:
request.refreshLocalBaseRefOnWorktreeCreate ??
hostSettings.refreshLocalBaseRefOnWorktreeCreate,
localBaseRefSuggestionDismissed:
request.localBaseRefSuggestionDismissed ?? hostSettings.localBaseRefSuggestionDismissed
}
const pathSettings = getWorktreePathSettings(repo, settings, getWorktreeMirrorDistro(store, repo))
const gitExecOptions = getLocalProjectGitExecOptions(store, repo)
@@ -43,6 +43,8 @@ export type RuntimeManagedWorktreeCreateArgs = {
workspaceStatus?: string
manualOrder?: number
nestWorkspaces?: boolean
refreshLocalBaseRefOnWorktreeCreate?: boolean
localBaseRefSuggestionDismissed?: boolean
sparseCheckout?: { directories: string[]; presetId?: string }
pushTarget?: GitPushTarget
runHooks?: boolean
@@ -56,7 +56,9 @@ describe('worktree remote runtime mutations', () => {
settings: {
...store.getState().settings!,
activeRuntimeEnvironmentId: 'env-1',
nestWorkspaces: false
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: true,
localBaseRefSuggestionDismissed: false
},
worktreesByRepo: { repo1: [] }
})
@@ -88,6 +90,8 @@ describe('worktree remote runtime mutations', () => {
setupDecision: 'skip',
sparseCheckout: { directories: ['src'], presetId: 'preset-1' },
nestWorkspaces: false,
refreshLocalBaseRefOnWorktreeCreate: true,
localBaseRefSuggestionDismissed: false,
telemetrySource: 'sidebar',
displayName: 'Feature title',
linkedIssue: 123,
@@ -0,0 +1,25 @@
import type { GlobalSettings } from '../../../../../../shared/global-settings-types'
const CLIENT_WORKTREE_CREATE_SETTING_KEYS = [
'nestWorkspaces',
'refreshLocalBaseRefOnWorktreeCreate',
'localBaseRefSuggestionDismissed'
] as const
/** Client-profile settings a remote host applies to this create instead of its own. */
export type ClientWorktreeCreateSettings = Partial<
Pick<GlobalSettings, (typeof CLIENT_WORKTREE_CREATE_SETTING_KEYS)[number]>
>
export function pickClientWorktreeCreateSettings(
settings: GlobalSettings | null | undefined
): ClientWorktreeCreateSettings {
const picked: ClientWorktreeCreateSettings = {}
for (const key of CLIENT_WORKTREE_CREATE_SETTING_KEYS) {
const value = settings?.[key]
if (typeof value === 'boolean') {
picked[key] = value
}
}
return picked
}
@@ -8,6 +8,10 @@ import { adoptFromEndpoint } from '../../../adopt-from-endpoint'
import type { WorktreeSlice } from '../../worktree-helpers'
import type { WorktreeSliceGet, WorktreeSliceSet } from '../listing/worktree-slice-types'
import type { CreateWorktreeResult } from '../../../../../../shared/worktree/create-types'
import {
pickClientWorktreeCreateSettings,
type ClientWorktreeCreateSettings
} from './client-worktree-create-settings'
import {
CLIENT_WORKTREE_CREATE_MAX_ATTEMPTS,
getClientWorktreeCreateCandidate,
@@ -51,7 +55,7 @@ async function runCreateAttempt(
request: WorktreeCreateRequest,
attempt: WorktreeCreateAttempt,
target: RuntimeTarget,
nestWorkspaces?: boolean
clientCreateSettings: ClientWorktreeCreateSettings = {}
): Promise<CreateAttemptOutcome> {
const provisionedRoot = request.options?.provisionedRoot
const create = async (
@@ -79,8 +83,8 @@ async function runCreateAttempt(
'worktree.create',
{
...buildRuntimeWorktreeCreateParams(request, { ...attempt, parentWorkspace }),
// Why: the host's own store default would otherwise decide the layout over the user's choice.
...(nestWorkspaces === undefined ? {} : { nestWorkspaces })
// Why: the host's own store defaults would otherwise override the user's choices.
...clientCreateSettings
},
{ timeoutMs: 10 * 60_000 }
)
@@ -239,7 +243,7 @@ export function createCreateWorktree(
manualOrder
},
target,
get().settings?.nestWorkspaces
pickClientWorktreeCreateSettings(get().settings)
)
if (lostRequestedParent(outcome, parent, target)) {
warnParentDroppedOnce()
@@ -60,6 +60,9 @@ export const WorktreeCreate = z
manualOrder: OptionalFiniteNumber,
// Why: the client's "Nest Workspaces" choice; absent (older clients) keeps the host's own setting.
nestWorkspaces: z.boolean().optional(),
// Why: the client's Keep Local Main Up to Date choice; absent (older clients) keeps the host's.
refreshLocalBaseRefOnWorktreeCreate: z.boolean().optional(),
localBaseRefSuggestionDismissed: z.boolean().optional(),
sparseCheckout: z
.object({
directories: z.array(z.string()),