Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .playwright/scripts/__tests__/settings.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
42 changes: 42 additions & 0 deletions apps/buddy/src/app/router/__tests__/desktopPages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<ExtensionStatus[]>([]), 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)) {
Expand Down
9 changes: 7 additions & 2 deletions apps/buddy/src/app/router/useDesktopPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -22,13 +22,18 @@ export function useDesktopPages(router: Router, installed: Readonly<Ref<Extensio
return [...registered.values()].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
})
const current = computed(() => resolveDesktopPageId(router.currentRoute.value))
const lastSettingsLocation = shallowRef<string>()
watch(router.currentRoute, (route) => {
if (resolveDesktopPageId(route) === 'lexora.settings')
lastSettingsLocation.value = route.fullPath
}, { immediate: true, flush: 'sync' })
const context = computed<WorkbenchContextSnapshot>((previous) => {
const next = {
values: { ...definitions.value.find(page => page.id === current.value)?.context?.(router.currentRoute.value), 'page.id': current.value },
pages: definitions.value.map(page => ({ id: page.id, title: page.title(language.value) })),
}
return previous && JSON.stringify(previous) === JSON.stringify(next) ? previous : next
})
const navigation = computed<DesktopNavigationEntry[]>(() => 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<DesktopNavigationEntry[]>(() => 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 }
}