From 2b140a4d9bf639ef3ff377320db32e48c31cd787 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:28:54 -0700 Subject: [PATCH] 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 --- .../rpc/methods/worktree-create-args.ts | 6 ++++ .../runtime-local-worktree-create.test.ts | 36 +++++++++++++++++++ .../runtime/runtime-local-worktree-create.ts | 8 ++++- .../runtime-managed-worktree-create-types.ts | 2 ++ .../worktrees-remote-runtime-create.test.ts | 6 +++- .../create/client-worktree-create-settings.ts | 25 +++++++++++++ .../worktrees/create/create-worktree.ts | 12 ++++--- .../rpc-contract/worktree-create-params.ts | 3 ++ 8 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/store/slices/worktrees/create/client-worktree-create-settings.ts diff --git a/src/main/runtime/rpc/methods/worktree-create-args.ts b/src/main/runtime/rpc/methods/worktree-create-args.ts index e0d31ad8115..2df9873d5f7 100644 --- a/src/main/runtime/rpc/methods/worktree-create-args.ts +++ b/src/main/runtime/rpc/methods/worktree-create-args.ts @@ -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, diff --git a/src/main/runtime/runtime-local-worktree-create.test.ts b/src/main/runtime/runtime-local-worktree-create.test.ts index 07cb0654054..bd3b56d2ee1 100644 --- a/src/main/runtime/runtime-local-worktree-create.test.ts +++ b/src/main/runtime/runtime-local-worktree-create.test.ts @@ -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({ diff --git a/src/main/runtime/runtime-local-worktree-create.ts b/src/main/runtime/runtime-local-worktree-create.ts index 1b4cc1521b4..d96d2dc5b95 100644 --- a/src/main/runtime/runtime-local-worktree-create.ts +++ b/src/main/runtime/runtime-local-worktree-create.ts @@ -70,7 +70,13 @@ async function performRuntimeLocalWorktreeCreate(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) diff --git a/src/main/runtime/runtime-managed-worktree-create-types.ts b/src/main/runtime/runtime-managed-worktree-create-types.ts index 4fb51a896b2..d52aa221392 100644 --- a/src/main/runtime/runtime-managed-worktree-create-types.ts +++ b/src/main/runtime/runtime-managed-worktree-create-types.ts @@ -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 diff --git a/src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts b/src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts index 5cb2a15c0ea..242c6ad30eb 100644 --- a/src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts +++ b/src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts @@ -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, diff --git a/src/renderer/src/store/slices/worktrees/create/client-worktree-create-settings.ts b/src/renderer/src/store/slices/worktrees/create/client-worktree-create-settings.ts new file mode 100644 index 00000000000..99dd89ed456 --- /dev/null +++ b/src/renderer/src/store/slices/worktrees/create/client-worktree-create-settings.ts @@ -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 +> + +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 +} diff --git a/src/renderer/src/store/slices/worktrees/create/create-worktree.ts b/src/renderer/src/store/slices/worktrees/create/create-worktree.ts index 46c06d7aec9..5e8675b31a9 100644 --- a/src/renderer/src/store/slices/worktrees/create/create-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/create/create-worktree.ts @@ -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 { 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() diff --git a/src/shared/rpc-contract/worktree-create-params.ts b/src/shared/rpc-contract/worktree-create-params.ts index e2c72c7944f..5945a751e63 100644 --- a/src/shared/rpc-contract/worktree-create-params.ts +++ b/src/shared/rpc-contract/worktree-create-params.ts @@ -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()),