mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
fix: defer remote visibility defaults after startup
This commit is contained in:
@@ -58,6 +58,8 @@ function useStartupActions() {
|
||||
fetchWorktreeLineage: s.fetchWorktreeLineage,
|
||||
fetchOrcaProfiles: s.fetchOrcaProfiles,
|
||||
fetchSettings: s.fetchSettings,
|
||||
awaitOwnerWorktreeVisibilityDefaultsHydration:
|
||||
s.awaitOwnerWorktreeVisibilityDefaultsHydration,
|
||||
fetchKeybindings: s.fetchKeybindings,
|
||||
initGitHubCache: s.initGitHubCache,
|
||||
hydrateWorkspaceSession: s.hydrateWorkspaceSession,
|
||||
@@ -109,8 +111,10 @@ export function useAppStartupHydration(onOnboardingLoaded: (state: OnboardingSta
|
||||
try {
|
||||
// Why: nothing in the hydration chain reads profile state synchronously, so don't let it add a serial IPC round-trip before fetchSettings.
|
||||
void actions.fetchOrcaProfiles()
|
||||
// Why: repo/worktree hydration routes through settings.activeRuntimeEnvironmentId; load settings first so a persisted remote runtime doesn't hydrate stale local state.
|
||||
await timeRendererStartupStep('fetch-settings', () => actions.fetchSettings())
|
||||
// Why: publish local settings before persisted UI/catalog work; a saved remote owner's defaults can spend the full connect timeout.
|
||||
await timeRendererStartupStep('fetch-settings', () =>
|
||||
actions.fetchSettings({ deferOwnerWorktreeVisibilityDefaults: true })
|
||||
)
|
||||
// Why: hidden-at-launch PTYs can query OSC 10/11 before any pane mounts; publish view attributes as soon as settings exist so main's silent-until-push responder has data.
|
||||
publishTerminalViewAttributesAtAppStart(
|
||||
useAppStore.getState().settings,
|
||||
@@ -283,6 +287,10 @@ export function useAppStartupHydration(onOnboardingLoaded: (state: OnboardingSta
|
||||
void (async () => {
|
||||
try {
|
||||
try {
|
||||
// Why: remote rows must not render under a fallback visibility while their owner default is still loading.
|
||||
await timeRendererStartupStep('owner-visibility-defaults', () =>
|
||||
actions.awaitOwnerWorktreeVisibilityDefaultsHydration()
|
||||
)
|
||||
await timeRendererStartupStep('remote-catalog-refresh', async () => {
|
||||
await actions.fetchReposForAllHosts()
|
||||
await actions.fetchProjectGroupsForAllHosts()
|
||||
|
||||
@@ -30,7 +30,9 @@ describe('renderer startup runtime routing', () => {
|
||||
const relativeIndex = startupBlock.indexOf(needle)
|
||||
return relativeIndex === -1 ? -1 : startupBlockStart + relativeIndex
|
||||
}
|
||||
const settingsIndex = indexInStartupBlock('actions.fetchSettings()')
|
||||
const settingsIndex = indexInStartupBlock(
|
||||
'actions.fetchSettings({ deferOwnerWorktreeVisibilityDefaults: true })'
|
||||
)
|
||||
const uiGetIndex = indexInStartupBlock("timeRendererStartupStep('ui-get'")
|
||||
const hydrateUiIndex = indexInStartupBlock("timeRendererStartupSyncStep('hydrate-persisted-ui'")
|
||||
const localReposIndex = indexInStartupBlock(
|
||||
@@ -96,6 +98,14 @@ describe('renderer startup runtime routing', () => {
|
||||
expect(fullWorktreesIndex).toBeGreaterThan(
|
||||
source.indexOf("logRendererStartupDiagnostic('startup-hydration-done'")
|
||||
)
|
||||
const ownerDefaultsIndex = source.indexOf(
|
||||
'actions.awaitOwnerWorktreeVisibilityDefaultsHydration()'
|
||||
)
|
||||
const remoteCatalogIndex = source.indexOf("timeRendererStartupStep('remote-catalog-refresh'")
|
||||
expect(ownerDefaultsIndex).toBeGreaterThan(
|
||||
source.indexOf("logRendererStartupDiagnostic('startup-hydration-done'")
|
||||
)
|
||||
expect(ownerDefaultsIndex).toBeLessThan(remoteCatalogIndex)
|
||||
// Why: the deferred full scan must be followed by a re-prune so deleted-worktree visit
|
||||
// timestamps for non-session repos are dropped once every repo is authoritative.
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { AppState } from '../types'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { hydrateOwnerWorktreeVisibilityDefaults } from './worktree-visibility-owner-settings'
|
||||
|
||||
export type SettingsStateSetter = Parameters<StateCreator<AppState, [], []>>[0]
|
||||
type SettingsStateGetter = Parameters<StateCreator<AppState, [], []>>[1]
|
||||
const completedOwnerVisibilityDefaultsHydration = Promise.resolve()
|
||||
const ownerVisibilityDefaultsHydrationByStore = new WeakMap<SettingsStateGetter, Promise<void>>()
|
||||
|
||||
export type FetchSettingsOptions = {
|
||||
deferOwnerWorktreeVisibilityDefaults?: boolean
|
||||
}
|
||||
|
||||
export function startOwnerWorktreeVisibilityDefaultsHydration(args: {
|
||||
settings: GlobalSettings
|
||||
deferPublication: boolean
|
||||
shouldPublish: () => boolean
|
||||
set: SettingsStateSetter
|
||||
get: SettingsStateGetter
|
||||
}): Promise<void> {
|
||||
if (args.deferPublication) {
|
||||
args.set((state) => ({
|
||||
settings: args.settings,
|
||||
worktreeVisibilityDefaultsByHost: args.settings.worktreeVisibilityDefaults
|
||||
? {
|
||||
...state.worktreeVisibilityDefaultsByHost,
|
||||
[LOCAL_EXECUTION_HOST_ID]: args.settings.worktreeVisibilityDefaults
|
||||
}
|
||||
: state.worktreeVisibilityDefaultsByHost
|
||||
}))
|
||||
}
|
||||
const hydration = hydrateOwnerWorktreeVisibilityDefaults(
|
||||
args.settings,
|
||||
args.get().worktreeVisibilityDefaultsByHost
|
||||
)
|
||||
.then((hydrated) => {
|
||||
if (!args.shouldPublish()) {
|
||||
return
|
||||
}
|
||||
args.set((state) => ({
|
||||
settings: hydrated.settings,
|
||||
worktreeVisibilityDefaultsByHost: {
|
||||
...state.worktreeVisibilityDefaultsByHost,
|
||||
...hydrated.defaultsByHost
|
||||
},
|
||||
worktreeVisibilityDefaultsSupportedRuntimeEnvironmentId:
|
||||
hydrated.supportedRuntimeEnvironmentId,
|
||||
worktreeVisibilitySourceDefaultsSupportedRuntimeEnvironmentId:
|
||||
hydrated.sourceDefaultsSupportedRuntimeEnvironmentId
|
||||
}))
|
||||
})
|
||||
.catch((err) => console.error('Failed to fetch settings:', err))
|
||||
ownerVisibilityDefaultsHydrationByStore.set(args.get, hydration)
|
||||
return hydration
|
||||
}
|
||||
|
||||
export function awaitOwnerWorktreeVisibilityDefaultsHydration(
|
||||
get: SettingsStateGetter
|
||||
): Promise<void> {
|
||||
return (
|
||||
ownerVisibilityDefaultsHydrationByStore.get(get) ?? completedOwnerVisibilityDefaultsHydration
|
||||
)
|
||||
}
|
||||
@@ -35,7 +35,8 @@ it('preserves host defaults added while owner hydration is in flight', async ()
|
||||
get: vi.fn().mockResolvedValue({ activeRuntimeEnvironmentId: 'env-1' })
|
||||
},
|
||||
runtimeEnvironments: {
|
||||
call: vi.fn().mockReturnValue(ownerRead)
|
||||
call: vi.fn().mockReturnValue(ownerRead),
|
||||
list: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -58,3 +59,50 @@ it('preserves host defaults added while owner hydration is in flight', async ()
|
||||
'runtime:env-2': { external: 'show' }
|
||||
})
|
||||
})
|
||||
|
||||
it('publishes startup settings before remote owner hydration and local catalog work', async () => {
|
||||
markRuntimeEnvironmentCompatible('env-1')
|
||||
let resolveOwnerRead!: (value: unknown) => void
|
||||
const ownerRead = new Promise((resolve) => (resolveOwnerRead = resolve))
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
settings: {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
activeRuntimeEnvironmentId: 'env-1',
|
||||
worktreeVisibilityDefaults: { external: 'show' }
|
||||
}),
|
||||
set: vi.fn().mockResolvedValue({
|
||||
activeRuntimeEnvironmentId: 'env-1',
|
||||
pluginSystemEnabled: true,
|
||||
worktreeVisibilityDefaults: { external: 'show' }
|
||||
})
|
||||
},
|
||||
runtimeEnvironments: {
|
||||
call: vi.fn().mockReturnValue(ownerRead),
|
||||
list: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = createTestStore()
|
||||
let localCatalogStarted = false
|
||||
const startup = (async () => {
|
||||
await store.getState().fetchSettings({ deferOwnerWorktreeVisibilityDefaults: true })
|
||||
localCatalogStarted = true
|
||||
})()
|
||||
await vi.waitFor(() => expect(window.api.runtimeEnvironments.call).toHaveBeenCalled())
|
||||
|
||||
expect(store.getState().settings?.activeRuntimeEnvironmentId).toBe('env-1')
|
||||
expect(store.getState().worktreeVisibilityDefaultsByHost.local).toEqual({ external: 'show' })
|
||||
expect(localCatalogStarted).toBe(true)
|
||||
|
||||
await store.getState().updateSettingsOrThrow({ pluginSystemEnabled: true })
|
||||
resolveOwnerRead({
|
||||
ok: true,
|
||||
result: { settings: { worktreeVisibilityDefaults: { external: 'hide' } } },
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
await startup
|
||||
await store.getState().awaitOwnerWorktreeVisibilityDefaultsHydration()
|
||||
expect(store.getState().settings?.pluginSystemEnabled).toBe(true)
|
||||
expect(store.getState().worktreeVisibilityDefaultsByHost['runtime:env-1']).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
hydrateOwnerWorktreeVisibilityDefaults,
|
||||
type WorktreeVisibilityDefaultsByHost
|
||||
} from './worktree-visibility-owner-settings'
|
||||
import * as ownerHydration from './settings-owner-hydration-publication'
|
||||
import { persistVisibilityAwareSettings } from './worktree-visibility-settings-write'
|
||||
import { getSettingsFocusedExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
@@ -40,7 +41,8 @@ export type SettingsSlice = SettingsSearchState & {
|
||||
worktreeVisibilityDefaultsByHost: WorktreeVisibilityDefaultsByHost
|
||||
worktreeVisibilityDefaultsSupportedRuntimeEnvironmentId: string | null
|
||||
worktreeVisibilitySourceDefaultsSupportedRuntimeEnvironmentId: string | null
|
||||
fetchSettings: () => Promise<void>
|
||||
fetchSettings: (options?: ownerHydration.FetchSettingsOptions) => Promise<void>
|
||||
awaitOwnerWorktreeVisibilityDefaultsHydration: () => Promise<void>
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void>
|
||||
updateSettingsOrThrow: (updates: Partial<GlobalSettings>) => Promise<void>
|
||||
setActiveRuntimeEnvironmentPreference: (environmentId: string | null) => Promise<boolean>
|
||||
@@ -50,7 +52,6 @@ type LegacyTerminalScrollbackSettingsUpdate = Partial<GlobalSettings> & {
|
||||
terminalScrollbackBytes?: unknown
|
||||
}
|
||||
|
||||
type SettingsStateSetter = Parameters<StateCreator<AppState, [], [], SettingsSlice>>[0]
|
||||
let ownerSettingsHydrationGeneration = 0
|
||||
|
||||
function normalizeRuntimeEnvironmentId(value: string | null | undefined): string | null {
|
||||
@@ -134,7 +135,7 @@ function normalizeSettingsUpdates(
|
||||
}
|
||||
|
||||
async function persistSettingsUpdates(
|
||||
set: SettingsStateSetter,
|
||||
set: ownerHydration.SettingsStateSetter,
|
||||
updates: Partial<GlobalSettings>,
|
||||
currentSettings: GlobalSettings | null,
|
||||
supportedRuntimeEnvironmentId: string | null,
|
||||
@@ -185,27 +186,24 @@ export const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice>
|
||||
worktreeVisibilitySourceDefaultsSupportedRuntimeEnvironmentId: null,
|
||||
...createSettingsSearchState((state) => set(state)),
|
||||
|
||||
fetchSettings: async () => {
|
||||
fetchSettings: async (options) => {
|
||||
const generation = ++ownerSettingsHydrationGeneration
|
||||
try {
|
||||
const hydrated = await hydrateOwnerWorktreeVisibilityDefaults(
|
||||
(await window.api.settings.get()) as GlobalSettings,
|
||||
get().worktreeVisibilityDefaultsByHost
|
||||
)
|
||||
const localSettings = (await window.api.settings.get()) as GlobalSettings
|
||||
if (generation !== ownerSettingsHydrationGeneration) {
|
||||
return
|
||||
}
|
||||
set((state) => ({
|
||||
settings: hydrated.settings,
|
||||
worktreeVisibilityDefaultsByHost: {
|
||||
...state.worktreeVisibilityDefaultsByHost,
|
||||
...hydrated.defaultsByHost
|
||||
},
|
||||
worktreeVisibilityDefaultsSupportedRuntimeEnvironmentId:
|
||||
hydrated.supportedRuntimeEnvironmentId,
|
||||
worktreeVisibilitySourceDefaultsSupportedRuntimeEnvironmentId:
|
||||
hydrated.sourceDefaultsSupportedRuntimeEnvironmentId
|
||||
}))
|
||||
const ownerVisibilityDefaultsHydration =
|
||||
ownerHydration.startOwnerWorktreeVisibilityDefaultsHydration({
|
||||
settings: localSettings,
|
||||
deferPublication: options?.deferOwnerWorktreeVisibilityDefaults === true,
|
||||
shouldPublish: () => generation === ownerSettingsHydrationGeneration,
|
||||
set,
|
||||
get
|
||||
})
|
||||
if (!options?.deferOwnerWorktreeVisibilityDefaults) {
|
||||
await ownerVisibilityDefaultsHydration
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch settings:', err)
|
||||
}
|
||||
@@ -223,6 +221,9 @@ export const createSettingsSlice: StateCreator<AppState, [], [], SettingsSlice>
|
||||
}
|
||||
},
|
||||
|
||||
awaitOwnerWorktreeVisibilityDefaultsHydration: () =>
|
||||
ownerHydration.awaitOwnerWorktreeVisibilityDefaultsHydration(get),
|
||||
|
||||
updateSettings: async (updates) => {
|
||||
const generation = ++ownerSettingsHydrationGeneration
|
||||
const visibilityOwnerHostId = getSettingsFocusedExecutionHostId(get().settings)
|
||||
|
||||
Reference in New Issue
Block a user