diff --git a/.playwright/scripts/__tests__/settings.e2e.mjs b/.playwright/scripts/__tests__/settings.e2e.mjs index b1e52fd54..aa8b880f4 100644 --- a/.playwright/scripts/__tests__/settings.e2e.mjs +++ b/.playwright/scripts/__tests__/settings.e2e.mjs @@ -78,6 +78,28 @@ test('prompt loading can retry and late responses do not replace another setting expect(diagnostics.console.filter(item => item.type === 'pageerror')).toEqual([]) }) +test('settings navigation returns to the last page and still allows explicit page selection', async ({ buddy }) => { + const instance = await buddy.createInstance('settings-navigation') + const { page, diagnostics } = await instance.launch() + const navigation = page.locator('.desktop-app-sidebar') + const title = page.locator('.desktop-settings-page__title') + await navigation.getByRole('button', { name: '设置', exact: true }).click() + await expect(title).toHaveText('常规') + + for (const [name, category] of [['快捷键', 'shortcuts'], ['外观', 'appearance'], ['常规', 'general']]) { + await page.locator('.desktop-settings-sidebar').getByRole('link', { name, exact: true }).click() + await expect(title).toHaveText(name) + await navigation.getByRole('button', { name: '任务', exact: true }).click() + await expect(page).toHaveURL(/#\/tasks$/) + await navigation.getByRole('button', { name: '设置', exact: true }).click() + await expect(page).toHaveURL(new RegExp(`#/settings/${category}$`)) + await expect(title).toHaveText(name) + await navigation.getByRole('button', { name: '设置', exact: true }).click() + await expect(title).toHaveText(name) + } + expect(diagnostics.console.filter(item => item.type === 'pageerror')).toEqual([]) +}) + test('builtin settings preserve legacy navigation, failed-save rollback and restart persistence', async ({ buddy }) => { const instance = await buddy.createInstance('settings') let desktop = await instance.launch() diff --git a/apps/buddy/src/app/router/__tests__/desktopPages.spec.ts b/apps/buddy/src/app/router/__tests__/desktopPages.spec.ts index 24f7f28cb..1d5055c94 100644 --- a/apps/buddy/src/app/router/__tests__/desktopPages.spec.ts +++ b/apps/buddy/src/app/router/__tests__/desktopPages.spec.ts @@ -6,6 +6,7 @@ import { builtinSettingsCatalog } from '@buddy-shared/settings/settingsCatalog' import { expect, it } from 'vitest' import { shallowRef } from 'vue' import { createMemoryHistory, createRouter } from 'vue-router' +import { settingsPage } from '@/modules/settings/routes' import { desktopPageRoutes } from '@/shared/navigation/desktopPages' import { desktopRouteLocations } from '@/shared/navigation/desktopRoutes' import { createDesktopRouter } from '../index' @@ -33,6 +34,47 @@ it('derives navigation and context from registered pages, including future modul expect(() => desktopPageRoutes([settings, settings])).toThrow('already registered') }) +it('restores the last successful settings location without overriding explicit destinations', async () => { + const settings: DesktopPageContribution = { + ...settingsPage, + routes: [{ + path: '/settings', + component: { render: () => null }, + children: [ + { path: 'general', name: 'desktop.settings.general', component: { render: () => null } }, + { path: ':category', component: { render: () => null } }, + { path: 'models/:providerId', component: { render: () => null } }, + { path: 'plugins/:moduleId', component: { render: () => null } }, + ], + }], + } + const router = createRouter({ history: createMemoryHistory(), routes: desktopPageRoutes([page('lexora.tasks', '/tasks'), settings]) }) + const pages = useDesktopPages(router, shallowRef([]), shallowRef('en-US' as const)) + const settingsLocation = () => pages.navigation.value.find(entry => entry.id === settings.id)!.location + expect(router.resolve(settingsLocation()).fullPath).toBe('/settings/general') + await router.push(settingsLocation()) + expect(router.currentRoute.value.fullPath).toBe('/settings/general') + + for (const location of ['/settings/appearance', '/settings/models/test.provider?group=details#credentials', '/settings/plugins/test.preferences?group=display']) { + await router.push(location) + await router.push('/tasks') + await router.push(settingsLocation()) + expect(router.currentRoute.value.fullPath).toBe(location) + } + + const removeGuard = router.beforeEach(to => to.path !== '/settings/notifications') + await router.push('/settings/notifications') + await router.push('/tasks') + expect(settingsLocation()).toBe('/settings/plugins/test.preferences?group=display') + removeGuard() + + await router.push(desktopRouteLocations.settings()) + expect(router.currentRoute.value.fullPath).toBe('/settings/general') + await router.push('/tasks') + await router.push(settingsLocation()) + expect(router.currentRoute.value.fullPath).toBe('/settings/general') +}) + it('allows cold-start access only for trusted runtime-independent settings routes', () => { const router = createDesktopRouter(createMemoryHistory()) for (const [category, capability] of Object.entries(builtinSettingsCatalog)) { diff --git a/apps/buddy/src/app/router/useDesktopPages.ts b/apps/buddy/src/app/router/useDesktopPages.ts index a745e3f32..2fda1b270 100644 --- a/apps/buddy/src/app/router/useDesktopPages.ts +++ b/apps/buddy/src/app/router/useDesktopPages.ts @@ -5,7 +5,7 @@ import type { Router } from 'vue-router' import type { BuddyLocale } from '@/i18n/buddyI18n' import type { DesktopNavigationEntry, DesktopPageDefinition } from '@/shared/navigation/desktopPages' import { matchesWorkbenchContext } from '@buddy-shared/workbench/workbenchContext' -import { computed } from 'vue' +import { computed, shallowRef, watch } from 'vue' import { extensionPageDefinitions } from '@/modules/extensions/routes' import { resolveDesktopPageId } from '@/shared/navigation/desktopPages' @@ -22,6 +22,11 @@ export function useDesktopPages(router: Router, installed: Readonly a.order - b.order || a.id.localeCompare(b.id)) }) const current = computed(() => resolveDesktopPageId(router.currentRoute.value)) + const lastSettingsLocation = shallowRef() + watch(router.currentRoute, (route) => { + if (resolveDesktopPageId(route) === 'lexora.settings') + lastSettingsLocation.value = route.fullPath + }, { immediate: true, flush: 'sync' }) const context = computed((previous) => { const next = { values: { ...definitions.value.find(page => page.id === current.value)?.context?.(router.currentRoute.value), 'page.id': current.value }, @@ -29,6 +34,6 @@ export function useDesktopPages(router: Router, installed: Readonly(() => definitions.value.filter(page => matchesWorkbenchContext(page.when, context.value.values)).map(page => ({ id: page.id, title: page.title(language.value), location: page.location, icon: page.icon, extensionId: page.extensionId, active: page.id === current.value }))) + const navigation = computed(() => definitions.value.filter(page => matchesWorkbenchContext(page.when, context.value.values)).map(page => ({ id: page.id, title: page.title(language.value), location: page.id === 'lexora.settings' ? lastSettingsLocation.value ?? page.location : page.location, icon: page.icon, extensionId: page.extensionId, active: page.id === current.value }))) return { current, context, navigation } }