diff --git a/src/main/index.ts b/src/main/index.ts index 9278eb043..312b8bfd9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1766,7 +1766,11 @@ app.whenReady().then(async () => { } ) electronApp.setAppUserModelId(devInstanceIdentity.appUserModelId) - app.setName(devInstanceIdentity.name) + // Why: setName drives the macOS safeStorage Keychain item name. Use the stable + // appName (not the per-branch `name`) so dev branches share one key and don't + // re-prompt per branch; the per-branch label still shows via window title, + // renderer identity, and the app-menu label passed to registerAppMenu below. + app.setName(devInstanceIdentity.appName) // Why: managed WSL launchers live outside the Windows app bundle, so keep // their launcher and bridge contract synchronized across app updates. @@ -2138,6 +2142,7 @@ app.whenReady().then(async () => { logStartupMilestone('i18n-ready') registerAppMenu({ + appMenuLabel: devInstanceIdentity.name, onCheckForUpdates: (options) => runUserInitiatedUpdateCheck(options), onBeforeReload: ({ ignoreCache, webContentsId }) => { if (mainWindow?.webContents.id === webContentsId) { diff --git a/src/main/menu/register-app-menu.ts b/src/main/menu/register-app-menu.ts index c36aca697..2f3a3d70a 100644 --- a/src/main/menu/register-app-menu.ts +++ b/src/main/menu/register-app-menu.ts @@ -37,6 +37,9 @@ type RegisterAppMenuOptions = { onToggleAppearance: (key: AppearanceMenuKey) => void getAppearanceState: () => AppearanceMenuState getKeybindings?: () => KeybindingOverrides | undefined + // Why: the macOS app-menu title. Passed the per-branch dev label since + // app.name is now pinned to a stable value for Keychain-key stability. + appMenuLabel?: string } function buildAndApplyMenu(options: RegisterAppMenuOptions): void { @@ -130,7 +133,7 @@ function buildAndApplyMenu(options: RegisterAppMenuOptions): void { // redundant "Orca" entry with roles that don't apply, so we omit it there // and distribute its items across File / Help instead. const macAppMenu: Electron.MenuItemConstructorOptions = { - label: app.name, + label: options.appMenuLabel ?? app.name, submenu: [ { role: 'about' }, checkForUpdatesItem, diff --git a/src/main/startup/dev-instance-identity.test.ts b/src/main/startup/dev-instance-identity.test.ts index 2021dfed2..2b06e85dd 100644 --- a/src/main/startup/dev-instance-identity.test.ts +++ b/src/main/startup/dev-instance-identity.test.ts @@ -5,6 +5,7 @@ describe('dev-instance-identity', () => { it('keeps packaged identity stable', () => { expect(getDevInstanceIdentity(false, {})).toMatchObject({ name: 'Orca', + appName: 'Orca', isDev: false, devLabel: null, dockBadgeLabel: null, @@ -12,6 +13,18 @@ describe('dev-instance-identity', () => { }) }) + it('pins a stable dev appName across branches so the safeStorage key does not churn', () => { + const a = getDevInstanceIdentity(true, { ORCA_DEV_BRANCH: 'feature/a' }) + const b = getDevInstanceIdentity(true, { ORCA_DEV_BRANCH: 'feature/b' }) + + // Per-branch label differs (window title / app menu)... + expect(a.name).not.toBe(b.name) + // ...but the Keychain-driving appName is identical and distinct from prod. + expect(a.appName).toBe('Orca Dev') + expect(b.appName).toBe('Orca Dev') + expect(a.appName).not.toBe('Orca') + }) + it('derives a readable dev label from worktree and branch env', () => { const identity = getDevInstanceIdentity(true, { ORCA_DEV_REPO_ROOT: '/repo/worktrees/dev-indicator', diff --git a/src/main/startup/dev-instance-identity.ts b/src/main/startup/dev-instance-identity.ts index b375e88bb..6d13541ea 100644 --- a/src/main/startup/dev-instance-identity.ts +++ b/src/main/startup/dev-instance-identity.ts @@ -8,6 +8,11 @@ const MAX_LABEL_LENGTH = 80 export type DevInstanceIdentity = AppIdentity & { appUserModelId: string + // Why: drives app.setName → the macOS safeStorage Keychain item name + // (" Safe Storage"). Kept stable across dev branches (unlike the + // per-branch `name`) so every dev instance shares one Keychain key instead of + // creating a new one per branch and re-prompting. Distinct from prod's 'Orca'. + appName: string } function cleanEnvValue(value: string | undefined): string | null { @@ -50,6 +55,7 @@ export function getDevInstanceIdentity( if (!isDev) { return { name: BASE_APP_NAME, + appName: BASE_APP_NAME, isDev: false, devLabel: null, devBranch: null, @@ -71,6 +77,10 @@ export function getDevInstanceIdentity( return { name: dockTitle, + // Why: one stable Keychain key ('Orca Dev Safe Storage') for all dev + // branches; the per-branch identity still shows via `name` (window title, + // app menu, renderer label). + appName: `${BASE_APP_NAME} Dev`, isDev: true, devLabel, devBranch: branch,