feat: implement core routing architecture with hybrid strategy (#11460)

This commit is contained in:
MyPrototypeWhat 2026-01-06 16:55:50 +08:00 committed by GitHub
parent d60ed5d9e4
commit d9f413b195
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 3468 additions and 418 deletions

View File

@ -52,6 +52,7 @@
"!src/main/integration/**",
"!**/tailwind.css",
"!**/package.json",
"!src/renderer/src/routeTree.gen.ts",
"!.zed/**"
],
"indentStyle": "space",
@ -82,7 +83,7 @@
},
"linter": {
"enabled": true,
"includes": ["!**/tailwind.css", "src/renderer/**/*.{tsx,ts}"],
"includes": ["!**/tailwind.css", "!src/renderer/src/routeTree.gen.ts", "src/renderer/**/*.{tsx,ts}"],
// only enable sorted tailwind css rule. used as formatter instead of linter
"rules": {
"nursery": {

View File

@ -1,3 +1,4 @@
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react-swc'
import { CodeInspectorPlugin } from 'code-inspector-plugin'
import { defineConfig } from 'electron-vite'
@ -80,6 +81,12 @@ export default defineConfig({
},
renderer: {
plugins: [
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
routesDirectory: resolve('src/renderer/src/routes'),
generatedRouteTree: resolve('src/renderer/src/routeTree.gen.ts')
}),
(async () => (await import('@tailwindcss/vite')).default())(),
react({
tsDecorators: true

View File

@ -67,6 +67,7 @@ export default defineConfig([
'src/main/integration/cherryai/index.js',
'src/main/integration/nutstore/sso/lib/**',
'src/renderer/src/ui/**',
'src/renderer/src/routeTree.gen.ts',
'packages/**/dist'
]
},

View File

@ -178,7 +178,9 @@
"@swc/plugin-styled-components": "^8.0.4",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-router": "^1.139.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/router-plugin": "^1.139.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@ -364,8 +366,6 @@
"react-markdown": "^10.1.0",
"react-player": "^3.3.1",
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-transition-group": "^4.4.5",
"redux": "^5.0.1",

View File

@ -225,11 +225,11 @@ export const DefaultSharedCache: SharedCacheSchema = {
* This ensures type safety and prevents key conflicts
*/
export type RendererPersistCacheSchema = {
'example_scope.example_key': string
'ui.tab.state': CacheValueTypes.TabsState
}
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
'example_scope.example_key': 'example default value'
'ui.tab.state': { tabs: [], activeTabId: '' }
}
// ============================================================================

View File

@ -17,3 +17,38 @@ export type CacheActiveSearches = Record<string, WebSearchStatus>
// The actual type checking will be done at runtime by the cache system
export type CacheMinAppType = MinAppType
export type CacheTopic = Topic
/**
* Tab type for browser-like tabs
*
* - 'route': Internal app routes rendered via MemoryRouter
* - 'webview': External web content rendered via Electron webview
*/
export type TabType = 'route' | 'webview'
/**
* Tab saved state for hibernation recovery
*/
export interface TabSavedState {
scrollPosition?: number
// 其他必要草稿字段可在此扩展
}
export interface Tab {
id: string
type: TabType
url: string
title: string
icon?: string
metadata?: Record<string, unknown>
// LRU 字段
lastAccessTime?: number // open/switch 时更新
isDormant?: boolean // 是否已休眠
isPinned?: boolean // 是否置顶(豁免 LRU
savedState?: TabSavedState // 休眠前保存的状态
}
export interface TabsState {
tabs: Tab[]
activeTabId: string
}

View File

@ -412,9 +412,15 @@ importers:
'@tanstack/react-query':
specifier: ^5.85.5
version: 5.90.16(react@19.2.3)
'@tanstack/react-router':
specifier: ^1.139.3
version: 1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-virtual':
specifier: ^3.13.12
version: 3.13.14(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/router-plugin':
specifier: ^1.139.3
version: 1.145.7(@tanstack/react-router@1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
'@testing-library/dom':
specifier: ^10.4.0
version: 10.4.1
@ -970,12 +976,6 @@ importers:
react-redux:
specifier: ^9.1.2
version: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1)
react-router:
specifier: '6'
version: 6.30.2(react@19.2.3)
react-router-dom:
specifier: '6'
version: 6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-spinners:
specifier: ^0.14.1
version: 0.14.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -1311,16 +1311,16 @@ importers:
version: 2.8.7(@types/react@19.2.7)(framer-motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)
'@storybook/addon-docs':
specifier: ^10.0.5
version: 10.1.11(@types/react@19.2.7)(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
version: 10.1.11(@types/react@19.2.7)(esbuild@0.25.12)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@storybook/addon-themes':
specifier: ^10.0.5
version: 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
version: 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@storybook/react':
specifier: ^10.1.11
version: 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
version: 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
'@storybook/react-vite':
specifier: ^10.0.5
version: 10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
version: 10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@svgr/core':
specifier: ^8.1.0
version: 8.1.0(typescript@5.8.3)
@ -1356,7 +1356,7 @@ importers:
version: 5.27.0(patch_hash=cdc383bd0d9b9fe0df2ce7b1f1d4ead200012b7f9517d9257b4ea0a5b324e243)(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
eslint-plugin-storybook:
specifier: 10.0.5
version: 10.0.5(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
version: 10.0.5(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
framer-motion:
specifier: ^12.23.12
version: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -1371,7 +1371,7 @@ importers:
version: 19.2.3(react@19.2.3)
storybook:
specifier: ^10.0.5
version: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
styled-components:
specifier: ^6.1.15
version: 6.1.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -1907,6 +1907,18 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-syntax-typescript@7.27.1':
resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-arrow-functions@7.27.1':
resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
engines: {node: '>=6.9.0'}
@ -5275,10 +5287,6 @@ packages:
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@remix-run/router@1.23.1':
resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==}
engines: {node: '>=14.0.0'}
'@replit/codemirror-lang-nix@6.0.1':
resolution: {integrity: sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==}
peerDependencies:
@ -6298,6 +6306,10 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@tanstack/history@1.145.7':
resolution: {integrity: sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ==}
engines: {node: '>=12'}
'@tanstack/query-core@5.90.16':
resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==}
@ -6306,6 +6318,19 @@ packages:
peerDependencies:
react: ^18 || ^19
'@tanstack/react-router@1.145.7':
resolution: {integrity: sha512-0O+a4TjJSPXd2BsvDPwDPBKRQKYqNIBg5TAg9NzCteqJ0NXRxwohyqCksHqCEEtJe/uItwqmHoqkK4q5MDhEsA==}
engines: {node: '>=12'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
'@tanstack/react-store@0.8.0':
resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/react-virtual@3.11.3':
resolution: {integrity: sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==}
peerDependencies:
@ -6318,12 +6343,52 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/router-core@1.145.7':
resolution: {integrity: sha512-v6jx6JqVUBM0/FcBq1tX22xiPq8Ufc0PDEP582/4deYoq2/RYd+bZstANp3mGSsqdxE/luhoLYuuSQiwi/j1wA==}
engines: {node: '>=12'}
'@tanstack/router-generator@1.145.7':
resolution: {integrity: sha512-xg71c1WTku0ro0rgpJWh3Dt+ognV9qWe2KJHAPzrqfOYdUYu9sGq7Ri4jo8Rk0luXWZrWsrFdBP+9Jx6JH6zWA==}
engines: {node: '>=12'}
'@tanstack/router-plugin@1.145.7':
resolution: {integrity: sha512-Rimo0NragYKHwjoYX9JBLS8VkZD4D/LqzzLIlX9yz93lmWFRu/DbuS7fDZNqX1Ea8naNvo18DlySszYLzC8XDg==}
engines: {node: '>=12'}
peerDependencies:
'@rsbuild/core': '>=1.0.2'
'@tanstack/react-router': ^1.145.7
vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
vite-plugin-solid: ^2.11.10
webpack: '>=5.92.0'
peerDependenciesMeta:
'@rsbuild/core':
optional: true
'@tanstack/react-router':
optional: true
vite:
optional: true
vite-plugin-solid:
optional: true
webpack:
optional: true
'@tanstack/router-utils@1.143.11':
resolution: {integrity: sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA==}
engines: {node: '>=12'}
'@tanstack/store@0.8.0':
resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
'@tanstack/virtual-core@3.11.3':
resolution: {integrity: sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==}
'@tanstack/virtual-core@3.13.14':
resolution: {integrity: sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==}
'@tanstack/virtual-file-routes@1.145.4':
resolution: {integrity: sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ==}
engines: {node: '>=12'}
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@ -7571,6 +7636,9 @@ packages:
react-native-b4a:
optional: true
babel-dead-code-elimination@1.0.11:
resolution: {integrity: sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ==}
bail@1.0.5:
resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
@ -8081,6 +8149,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-es@2.0.0:
resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
@ -10039,6 +10110,10 @@ packages:
resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==}
engines: {node: '>= 18.0.0'}
isbot@5.1.32:
resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==}
engines: {node: '>=18'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -11742,6 +11817,11 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
prettier@3.7.4:
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'}
hasBin: true
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@ -12250,19 +12330,6 @@ packages:
'@types/react':
optional: true
react-router-dom@6.30.2:
resolution: {integrity: sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
react-router@6.30.2:
resolution: {integrity: sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-spinners@0.14.1:
resolution: {integrity: sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==}
peerDependencies:
@ -12760,6 +12827,16 @@ packages:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
seroval-plugins@1.4.2:
resolution: {integrity: sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA==}
engines: {node: '>=10'}
peerDependencies:
seroval: ^1.0
seroval@1.4.2:
resolution: {integrity: sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==}
engines: {node: '>=10'}
serve-static@2.2.1:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
@ -13248,6 +13325,9 @@ packages:
tiny-typed-emitter@2.1.0:
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@ -13779,6 +13859,46 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@7.3.0:
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -15270,6 +15390,16 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)':
dependencies:
'@babel/core': 7.28.5
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)':
dependencies:
'@babel/core': 7.28.5
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)':
dependencies:
'@babel/core': 7.28.5
@ -17664,11 +17794,11 @@ snapshots:
'@istanbuljs/schema@0.1.3': {}
'@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.8.3)':
'@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
glob: 11.1.0
react-docgen-typescript: 2.4.0(typescript@5.8.3)
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
optionalDependencies:
typescript: 5.8.3
@ -19646,8 +19776,6 @@ snapshots:
'@remirror/core-constants@3.0.0': {}
'@remix-run/router@1.23.1': {}
'@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.11.3)(@codemirror/state@6.5.3)(@codemirror/view@6.38.1)(@lezer/common@1.5.0)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.5)':
dependencies:
'@codemirror/autocomplete': 6.20.0
@ -20293,15 +20421,15 @@ snapshots:
'@standard-schema/utils@0.3.0': {}
'@storybook/addon-docs@10.1.11(@types/react@19.2.7)(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@storybook/addon-docs@10.1.11(@types/react@19.2.7)(esbuild@0.25.12)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3)
'@storybook/csf-plugin': 10.1.11(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@storybook/csf-plugin': 10.1.11(esbuild@0.25.12)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@storybook/react-dom-shim': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@storybook/react-dom-shim': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
@ -20310,32 +20438,32 @@ snapshots:
- vite
- webpack
'@storybook/addon-themes@10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@storybook/addon-themes@10.1.11(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
dependencies:
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
ts-dedent: 2.2.0
'@storybook/builder-vite@10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@storybook/builder-vite@10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@storybook/csf-plugin': 10.1.11(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@storybook/csf-plugin': 10.1.11(esbuild@0.25.12)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@vitest/mocker': 3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
ts-dedent: 2.2.0
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- esbuild
- msw
- rollup
- webpack
'@storybook/csf-plugin@10.1.11(esbuild@0.25.12)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@storybook/csf-plugin@10.1.11(esbuild@0.25.12)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.25.12
rollup: 4.55.1
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
'@storybook/global@5.0.0': {}
@ -20344,27 +20472,27 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@storybook/react-dom-shim@10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@storybook/react-dom-shim@10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
dependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@storybook/react-vite@10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)':
'@storybook/react-vite@10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(typescript@5.8.3)
'@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(typescript@5.8.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@rollup/pluginutils': 5.3.0(rollup@4.55.1)
'@storybook/builder-vite': 10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@storybook/react': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
'@storybook/builder-vite': 10.1.11(esbuild@0.25.12)(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(rollup@4.55.1)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
'@storybook/react': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)
empathic: 2.0.0
magic-string: 0.30.21
react: 19.2.3
react-docgen: 8.0.2
react-dom: 19.2.3(react@19.2.3)
resolve: 1.22.11
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tsconfig-paths: 4.2.0
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- esbuild
- msw
@ -20373,14 +20501,14 @@ snapshots:
- typescript
- webpack
'@storybook/react@10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)':
'@storybook/react@10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3)':
dependencies:
'@storybook/global': 5.0.0
'@storybook/react-dom-shim': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@storybook/react-dom-shim': 10.1.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
react: 19.2.3
react-docgen: 8.0.2
react-dom: 19.2.3(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
@ -20610,6 +20738,8 @@ snapshots:
tailwindcss: 4.1.18
vite: rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
'@tanstack/history@1.145.7': {}
'@tanstack/query-core@5.90.16': {}
'@tanstack/react-query@5.90.16(react@19.2.3)':
@ -20617,6 +20747,24 @@ snapshots:
'@tanstack/query-core': 5.90.16
react: 19.2.3
'@tanstack/react-router@1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/history': 1.145.7
'@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/router-core': 1.145.7
isbot: 5.1.32
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/store': 0.8.0
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
'@tanstack/react-virtual@3.11.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/virtual-core': 3.11.3
@ -20629,10 +20777,71 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@tanstack/router-core@1.145.7':
dependencies:
'@tanstack/history': 1.145.7
'@tanstack/store': 0.8.0
cookie-es: 2.0.0
seroval: 1.4.2
seroval-plugins: 1.4.2(seroval@1.4.2)
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
'@tanstack/router-generator@1.145.7':
dependencies:
'@tanstack/router-core': 1.145.7
'@tanstack/router-utils': 1.143.11
'@tanstack/virtual-file-routes': 1.145.4
prettier: 3.7.4
recast: 0.23.11
source-map: 0.7.6
tsx: 4.21.0
zod: 3.25.76
transitivePeerDependencies:
- supports-color
'@tanstack/router-plugin@1.145.7(@tanstack/react-router@1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
'@babel/template': 7.27.2
'@babel/traverse': 7.28.5
'@babel/types': 7.28.5
'@tanstack/router-core': 1.145.7
'@tanstack/router-generator': 1.145.7
'@tanstack/router-utils': 1.143.11
'@tanstack/virtual-file-routes': 1.145.4
babel-dead-code-elimination: 1.0.11
chokidar: 3.6.0
unplugin: 2.3.11
zod: 3.25.76
optionalDependencies:
'@tanstack/react-router': 1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
vite: rolldown-vite@7.3.0(@types/node@22.17.2)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
'@tanstack/router-utils@1.143.11':
dependencies:
'@babel/core': 7.28.5
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
ansis: 4.2.0
diff: 8.0.2
pathe: 2.0.3
tinyglobby: 0.2.15
transitivePeerDependencies:
- supports-color
'@tanstack/store@0.8.0': {}
'@tanstack/virtual-core@3.11.3': {}
'@tanstack/virtual-core@3.13.14': {}
'@tanstack/virtual-file-routes@1.145.4': {}
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.27.1
@ -22062,6 +22271,15 @@ snapshots:
msw: 2.12.7(@types/node@24.10.4)(typescript@5.8.3)
vite: rolldown-vite@7.3.0(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/mocker@3.2.4(msw@2.12.7(@types/node@24.10.4)(typescript@5.8.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
msw: 2.12.7(@types/node@24.10.4)(typescript@5.8.3)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
@ -22461,6 +22679,15 @@ snapshots:
b4a@1.7.3: {}
babel-dead-code-elimination@1.0.11:
dependencies:
'@babel/core': 7.28.5
'@babel/parser': 7.28.5
'@babel/traverse': 7.28.5
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
bail@1.0.5: {}
bail@2.0.2: {}
@ -23031,6 +23258,8 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-es@2.0.0: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
@ -24050,11 +24279,11 @@ snapshots:
dependencies:
eslint: 9.39.2(jiti@2.6.1)
eslint-plugin-storybook@10.0.5(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3):
eslint-plugin-storybook@10.0.5(eslint@9.39.2(jiti@2.6.1))(storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.8.3):
dependencies:
'@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3)
eslint: 9.39.2(jiti@2.6.1)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
storybook: 10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
transitivePeerDependencies:
- supports-color
- typescript
@ -25264,6 +25493,8 @@ snapshots:
isbinaryfile@5.0.4: {}
isbot@5.1.32: {}
isexe@2.0.0: {}
isexe@3.1.1: {}
@ -27320,6 +27551,8 @@ snapshots:
prettier@2.8.8: {}
prettier@3.7.4: {}
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@ -28016,18 +28249,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
react-router-dom@6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@remix-run/router': 1.23.1
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react-router: 6.30.2(react@19.2.3)
react-router@6.30.2(react@19.2.3):
dependencies:
'@remix-run/router': 1.23.1
react: 19.2.3
react-spinners@0.14.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
react: 19.2.3
@ -28557,7 +28778,6 @@ snapshots:
'@rollup/rollup-win32-x64-gnu': 4.55.1
'@rollup/rollup-win32-x64-msvc': 4.55.1
fsevents: 2.3.3
optional: true
rope-sequence@1.3.4: {}
@ -28674,6 +28894,12 @@ snapshots:
type-fest: 0.13.1
optional: true
seroval-plugins@1.4.2(seroval@1.4.2):
dependencies:
seroval: 1.4.2
seroval@1.4.2: {}
serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
@ -28896,7 +29122,7 @@ snapshots:
std-env@3.10.0: {}
storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
storybook@10.1.11(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@storybook/global': 5.0.0
'@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -28911,7 +29137,7 @@ snapshots:
use-sync-external-store: 1.6.0(react@19.2.3)
ws: 8.18.3
optionalDependencies:
prettier: 2.8.8
prettier: 3.7.4
transitivePeerDependencies:
- '@testing-library/dom'
- bufferutil
@ -29279,6 +29505,8 @@ snapshots:
tiny-typed-emitter@2.1.0: {}
tiny-warning@1.0.3: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@ -29869,6 +30097,22 @@ snapshots:
- tsx
- yaml
vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.55.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.4
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
tsx: 4.21.0
yaml: 2.8.2
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(esbuild@0.25.12)(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(@types/node@22.17.2)(typescript@5.8.3))(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3

View File

@ -7,13 +7,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { AppShell } from './components/layout/AppShell'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import Router from './Router'
const logger = loggerService.withContext('App.tsx')
@ -42,7 +42,7 @@ function App(): React.ReactElement {
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<Router />
<AppShell />
</TopViewContainer>
</PersistGate>
</CodeStyleProvider>

View File

@ -1,67 +0,0 @@
import '@renderer/databases'
import type { FC } from 'react'
import { useMemo } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import Sidebar from './components/app/Sidebar'
import { ErrorBoundary } from './components/ErrorBoundary'
import TabsContainer from './components/Tab/TabContainer'
import NavigationHandler from './handler/NavigationHandler'
import { useNavbarPosition } from './hooks/useNavbar'
import CodeToolsPage from './pages/code/CodeToolsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import LaunchpadPage from './pages/launchpad/LaunchpadPage'
import MinAppPage from './pages/minapps/MinAppPage'
import MinAppsPage from './pages/minapps/MinAppsPage'
import NotesPage from './pages/notes/NotesPage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
import TranslatePage from './pages/translate/TranslatePage'
const Router: FC = () => {
const { navbarPosition } = useNavbarPosition()
const routes = useMemo(() => {
return (
<ErrorBoundary>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/store" element={<AssistantPresetsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/notes" element={<NotesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps/:appId" element={<MinAppPage />} />
<Route path="/apps" element={<MinAppsPage />} />
<Route path="/code" element={<CodeToolsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} />
</Routes>
</ErrorBoundary>
)
}, [])
if (navbarPosition === 'left') {
return (
<HashRouter>
<Sidebar />
{routes}
<NavigationHandler />
</HashRouter>
)
}
return (
<HashRouter>
<NavigationHandler />
<TabsContainer>{routes}</TabsContainer>
</HashRouter>
)
}
export default Router

View File

@ -30,13 +30,13 @@ export const FreeTrialModelTag: FC<Props> = ({ model, showLabel = true }) => {
}
const onSelectProvider = () => {
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
NavigationService.navigate!({ to: `/settings/provider`, search: { id: providerId } })
}
const onNavigateProvider = (e: MouseEvent) => {
e.stopPropagation()
SelectModelPopup.hide()
NavigationService.navigate!(`/settings/provider?id=${providerId}`)
NavigationService.navigate?.({ to: '/settings/provider', search: { id: providerId } })
}
if (!showLabel) {

View File

@ -6,11 +6,11 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import type { MinAppType } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
interface Props {
@ -47,7 +47,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
const handleClick = () => {
if (isTopNavbar) {
// 顶部导航栏:导航到小程序页面
navigate(`/apps/${app.id}`)
navigate({ to: '/app/minapp/$appId', params: { appId: app.id } })
} else {
// 侧边导航栏:保持原有弹窗行为
openMinappKeepAlive(app)

View File

@ -3,9 +3,9 @@ import WebviewContainer from '@renderer/components/MinApp/WebviewContainer'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import { useLocation } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import React, { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
/**
@ -31,10 +31,10 @@ const MinAppTabsPool: React.FC = () => {
// 使用集中工具进行更稳健的路由判断
const isAppDetail = (() => {
const pathname = location.pathname
if (pathname === '/apps') return false
if (!pathname.startsWith('/apps/')) return false
const parts = pathname.split('/').filter(Boolean) // ['apps', '<id>', ...]
return parts.length >= 2
if (pathname === '/app/minapp') return false
if (!pathname.startsWith('/app/minapp/')) return false
const parts = pathname.split('/').filter(Boolean) // ['app', 'minapp', '<id>', ...]
return parts.length >= 3
})()
const shouldShow = isTopNavbar && isAppDetail

View File

@ -193,7 +193,7 @@ const PopupContainer: React.FC<Props> = ({ model, filter: baseFilter, showTagFil
e.stopPropagation()
setOpen(false)
resolve(undefined)
window.navigate(`/settings/provider?id=${p.id}`)
window.navigate({ to: '/settings/provider', search: { id: p.id } })
}}
/>
</Tooltip>

View File

@ -17,6 +17,7 @@ import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import type { MinAppType } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { useLocation, useNavigate } from '@tanstack/react-router'
import type { LRUCache } from 'lru-cache'
import {
FileSearch,
@ -36,7 +37,6 @@ import {
} from 'lucide-react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import MinAppIcon from '../Icons/MinAppIcon'
@ -200,17 +200,17 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const handleAddTab = () => {
hideMinappPopup()
navigate('/launchpad')
navigate({ to: '/launchpad' })
}
const handleSettingsClick = () => {
hideMinappPopup()
navigate(lastSettingsPath)
navigate({ to: lastSettingsPath })
}
const handleTabClick = (tab: Tab) => {
hideMinappPopup()
navigate(tab.path)
navigate({ to: tab.path })
}
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])

View File

@ -12,6 +12,7 @@ import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useSettings } from '@renderer/hooks/useSettings'
import { getSidebarIconLabel, getThemeModeLabel } from '@renderer/i18n/label'
import { isEmoji } from '@renderer/utils'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import {
Code,
@ -30,9 +31,9 @@ import {
} from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { useTabs } from '../../hooks/useTabs'
import UserPopup from '../Popups/UserPopup'
import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps'
@ -40,9 +41,11 @@ const Sidebar: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { pinned, minappShow } = useMinapps()
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
const { tabs, activeTabId, updateTab } = useTabs()
const { pathname } = useLocation()
const navigate = useNavigate()
// 获取当前 Tab 的 URL 作为 pathname
const activeTab = tabs.find((t) => t.id === activeTabId)
const pathname = activeTab?.url || '/'
const { theme, settedTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
@ -54,9 +57,12 @@ const Sidebar: FC = () => {
const showPinnedApps = pinned.length > 0 && visibleSidebarIcons.includes('minapp')
// 在当前 Tab 内跳转
const to = async (path: string) => {
await modelGenerating()
navigate(path)
if (activeTabId) {
updateTab(activeTabId, { url: path, title: getDefaultRouteTitle(path) })
}
}
const isFullscreen = useFullscreen()
@ -118,14 +124,16 @@ const Sidebar: FC = () => {
const MainMenus: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { minappShow } = useMinapps()
const { tabs, activeTabId, updateTab } = useTabs()
// 获取当前 Tab 的 URL 作为 pathname
const activeTab = tabs.find((t) => t.id === activeTabId)
const pathname = activeTab?.url || '/'
const { pathname } = useLocation()
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
const { defaultPaintingProvider } = useSettings()
const navigate = useNavigate()
const { theme } = useTheme()
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
const iconMap = {
@ -141,28 +149,35 @@ const MainMenus: FC = () => {
}
const pathMap = {
assistants: '/',
store: '/store',
paintings: `/paintings/${defaultPaintingProvider}`,
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files',
code_tools: '/code',
notes: '/notes'
assistants: '/app/chat',
store: '/app/assistant',
paintings: `/app/paintings/${defaultPaintingProvider}`,
translate: '/app/translate',
minapp: '/app/minapp',
knowledge: '/app/knowledge',
files: '/app/files',
code_tools: '/app/code',
notes: '/app/notes'
}
// 在当前 Tab 内跳转
const to = async (path: string) => {
await modelGenerating()
if (activeTabId) {
updateTab(activeTabId, { url: path, title: getDefaultRouteTitle(path) })
}
}
return visibleSidebarIcons.map((icon) => {
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
const isActive = isRoutes(path)
return (
<Tooltip key={icon} placement="right" content={getSidebarIconLabel(icon)} delay={800}>
<StyledLink
onClick={async () => {
hideMinappPopup()
await modelGenerating()
navigate(path)
await to(path)
}}>
<Icon theme={theme} className={isActive}>
{iconMap[icon]}

View File

@ -0,0 +1,111 @@
import '@renderer/databases'
import { Tabs, TabsList, TabsTrigger } from '@cherrystudio/ui'
import { cn } from '@renderer/utils'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { Plus, X } from 'lucide-react'
import { Activity } from 'react'
import { v4 as uuid } from 'uuid'
import { useTabs } from '../../hooks/useTabs'
import Sidebar from '../app/Sidebar'
import { TabRouter } from './TabRouter'
// Mock Webview component (TODO: Replace with actual MinApp/Webview)
const WebviewContainer = ({ url, isActive }: { url: string; isActive: boolean }) => (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<div className="flex h-full w-full flex-col items-center justify-center bg-background">
<div className="mb-2 font-bold text-lg">Webview App</div>
<code className="rounded bg-muted p-2">{url}</code>
</div>
</Activity>
)
export const AppShell = () => {
const { tabs, activeTabId, setActiveTab, closeTab, updateTab, addTab } = useTabs()
// Sync internal navigation back to tab state with default title (url may include search/hash)
const handleUrlChange = (tabId: string, url: string) => {
updateTab(tabId, { url, title: getDefaultRouteTitle(url) })
}
// 新增 Tab默认打开首页
const handleAddTab = () => {
addTab({
id: uuid(),
type: 'route',
url: '/',
title: getDefaultRouteTitle('/')
})
}
return (
<div className="flex h-screen w-screen flex-row overflow-hidden bg-background text-foreground">
{/* Zone 1: Sidebar */}
<Sidebar />
<div className="flex h-full w-full flex-1 flex-col overflow-hidden">
{/* Zone 2: Tab Bar */}
<Tabs value={activeTabId} onValueChange={setActiveTab} variant="line" className="w-full">
<header className="flex h-10 w-full items-center border-b bg-muted/5">
<TabsList className="flex h-full min-w-0 flex-1 justify-start gap-0 overflow-hidden">
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className={cn(
'group relative flex h-full min-w-0 max-w-[200px] flex-1 items-center justify-between gap-2 rounded-none border-r px-3 text-sm',
tab.id === activeTabId ? 'bg-background' : 'bg-transparent'
)}>
{/* TODO: pin功能,形式还未确定 */}
<span className={cn('truncate text-xs', tab.isDormant && 'opacity-60')}>{tab.title}</span>
{tabs.length > 1 && (
<div
role="button"
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}
className="ml-1 cursor-pointer rounded-sm p-0.5 opacity-0 hover:bg-muted-foreground/20 hover:opacity-100 group-hover:opacity-50">
<X className="size-3" />
</div>
)}
</TabsTrigger>
))}
{/* 新增 Tab 按钮 - 跟随最后一个 Tab */}
<button
type="button"
onClick={handleAddTab}
className="flex h-full shrink-0 items-center justify-center px-3 hover:bg-muted/50"
title="New Tab">
<Plus className="size-4" />
</button>
</TabsList>
</header>
</Tabs>
{/* Zone 3: Content Area - Multi MemoryRouter Architecture */}
<main className="relative flex-1 overflow-hidden bg-background">
{/* Route Tabs: Only render non-dormant tabs */}
{tabs
.filter((t) => t.type === 'route' && !t.isDormant)
.map((tab) => (
<TabRouter
key={tab.id}
tab={tab}
isActive={tab.id === activeTabId}
onUrlChange={(url) => handleUrlChange(tab.id, url)}
/>
))}
{/* Webview Tabs: Only render non-dormant tabs */}
{tabs
.filter((t) => t.type === 'webview' && !t.isDormant)
.map((tab) => (
<WebviewContainer key={tab.id} url={tab.url} isActive={tab.id === activeTabId} />
))}
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,52 @@
import type { Tab } from '@shared/data/cache/cacheValueTypes'
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router'
import { Activity } from 'react'
import { useEffect, useMemo } from 'react'
import { routeTree } from '../../routeTree.gen'
interface TabRouterProps {
tab: Tab
isActive: boolean
onUrlChange: (url: string) => void
}
/**
* TabRouter - Independent MemoryRouter for each Tab
*
* Each tab maintains its own router instance with isolated history,
* enabling true KeepAlive behavior via React 19's Activity component.
*/
export const TabRouter = ({ tab, isActive, onUrlChange }: TabRouterProps) => {
// Create independent router instance per tab (only once)
const router = useMemo(() => {
const history = createMemoryHistory({ initialEntries: [tab.url] })
return createRouter({ routeTree, history })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab.id])
// Sync internal navigation back to tab state
useEffect(() => {
return router.subscribe('onResolved', ({ toLocation }) => {
const nextHref = toLocation.href
if (nextHref !== tab.url) {
onUrlChange(nextHref)
}
})
}, [router, tab.url, onUrlChange])
// Navigate when tab.url changes externally (e.g., from Sidebar)
useEffect(() => {
const currentHref = router.state.location.href
if (tab.url !== currentHref) {
router.navigate({ to: tab.url })
}
}, [router, tab.url])
return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<div className="h-full w-full">
<RouterProvider router={router} />
</div>
</Activity>
)
}

View File

@ -2,8 +2,8 @@
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
import type { ToastUtilities } from '@cherrystudio/ui'
import type { UseNavigateResult } from '@tanstack/react-router'
import type { HookAPI } from 'antd/es/modal/useModal'
import type { NavigateFunction } from 'react-router-dom'
interface ImportMetaEnv {
VITE_RENDERER_INTEGRATED_MODEL: string
@ -18,7 +18,7 @@ declare global {
root: HTMLElement
modal: HookAPI
store: any
navigate: NavigateFunction
navigate: UseNavigateResult<string>
toast: ToastUtilities
agentTools: {
respondToPermission: (payload: {

View File

@ -1,8 +1,8 @@
import { useAppSelector } from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => {
const location = useLocation()
@ -17,7 +17,7 @@ const NavigationHandler: React.FC = () => {
if (location.pathname.startsWith('/settings')) {
return
}
navigate('/settings/provider')
navigate({ to: '/settings/provider' })
},
{
splitKey: '!',
@ -30,7 +30,7 @@ const NavigationHandler: React.FC = () => {
// Listen for navigate to About page event from macOS menu
useEffect(() => {
const handleNavigateToAbout = () => {
navigate('/settings/about')
navigate({ to: '/settings/about' })
}
const removeListener = window.electron.ipcRenderer.on(IpcChannel.Windows_NavigateToAbout, handleNavigateToAbout)

View File

@ -64,7 +64,7 @@ export function useAppInit() {
useEffect(() => {
window.api.getDataPathFromArgs().then((dataPath) => {
if (dataPath) {
window.navigate('/settings/data', { replace: true })
window.navigate({ to: '/settings/data', replace: true })
}
})
}, [])

View File

@ -13,8 +13,8 @@ window.electron.ipcRenderer.on(IpcChannel.Mcp_ServersChanged, (_event, servers)
window.electron.ipcRenderer.on(IpcChannel.Mcp_AddServer, (_event, server: MCPServer) => {
store.dispatch(addMCPServer(server))
NavigationService.navigate?.('/settings/mcp')
NavigationService.navigate?.(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)
NavigationService.navigate?.({ to: '/settings/mcp' })
NavigationService.navigate?.({ to: `/settings/mcp/settings/${encodeURIComponent(server.id)}` })
})
const selectMcpServers = (state: RootState) => state.mcp.servers

View File

@ -186,7 +186,7 @@ export const useMinappPopup = () => {
// Then navigate to the app tab using NavigationService
if (NavigationService.navigate) {
NavigationService.navigate(`/apps/${config.id}`)
NavigationService.navigate({ to: `/apps/${config.id}` })
}
} else {
// For side navbar, use the traditional popup system

View File

@ -0,0 +1,326 @@
import { loggerService } from '@logger'
import { TabLRUManager } from '@renderer/services/TabLRUManager'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { usePersistCache } from '../data/hooks/useCache'
import { uuid } from '../utils'
import { getDefaultRouteTitle } from '../utils/routeTitle'
// Re-export types from shared schema
export type { Tab, TabsState, TabType } from '@shared/data/cache/cacheValueTypes'
import type { Tab, TabSavedState, TabType } from '@shared/data/cache/cacheValueTypes'
const logger = loggerService.withContext('useTabs')
const DEFAULT_TAB: Tab = {
id: 'home',
type: 'route',
url: '/home',
title: getDefaultRouteTitle('/home'),
lastAccessTime: Date.now(),
isDormant: false
}
/**
* Options for opening a tab
*/
export interface OpenTabOptions {
/** Force open a new tab even if one with the same URL exists */
forceNew?: boolean
/** Tab title (defaults to URL path) */
title?: string
/** Tab type (defaults to 'route') */
type?: TabType
/** Custom tab ID (auto-generated if not provided) */
id?: string
}
export function useTabs() {
const [tabsState, setTabsState] = usePersistCache('ui.tab.state')
// LRU 管理器(单例)
const lruManagerRef = useRef<TabLRUManager | null>(null)
if (!lruManagerRef.current) {
lruManagerRef.current = new TabLRUManager()
}
const lruManager = lruManagerRef.current
// Ensure at least one default tab exists
useEffect(() => {
if (tabsState.tabs.length === 0) {
setTabsState({ tabs: [DEFAULT_TAB], activeTabId: DEFAULT_TAB.id })
}
}, [tabsState.tabs.length, setTabsState])
const tabs = useMemo(() => (tabsState.tabs.length > 0 ? tabsState.tabs : [DEFAULT_TAB]), [tabsState.tabs])
const activeTabId = tabsState.activeTabId || DEFAULT_TAB.id
/**
*
*/
const performHibernationCheck = useCallback(
(currentTabs: Tab[], newActiveTabId: string) => {
const toHibernate = lruManager.checkAndGetDormantCandidates(currentTabs, newActiveTabId)
if (toHibernate.length === 0) {
return currentTabs
}
// 批量休眠
return currentTabs.map((tab) => {
if (toHibernate.includes(tab.id)) {
logger.info('Tab hibernated', { tabId: tab.id, route: tab.url })
// TODO: 保存滚动位置等状态
const savedState: TabSavedState = { scrollPosition: 0 }
return { ...tab, isDormant: true, savedState }
}
return tab
})
},
[lruManager]
)
/**
*
*
* TODO: 目前 savedState
* -
* - 稿/
*/
const hibernateTab = useCallback(
(tabId: string) => {
const tab = tabsState.tabs.find((t) => t.id === tabId)
if (!tab || tab.isDormant) return
// TODO: 实现真实的状态捕获
const savedState: TabSavedState = { scrollPosition: 0 }
logger.info('Tab hibernated (manual)', { tabId, route: tab.url })
setTabsState({
...tabsState,
tabs: tabsState.tabs.map((t) => (t.id === tabId ? { ...t, isDormant: true, savedState } : t))
})
},
[tabsState, setTabsState]
)
/**
*
*
* TODO: 目前仅清除 isDormant
* - savedState
* - 稿/
*/
const wakeTab = useCallback(
(tabId: string) => {
const tab = tabsState.tabs.find((t) => t.id === tabId)
if (!tab || !tab.isDormant) return
logger.info('Tab awakened', { tabId, route: tab.url })
// TODO: 实现真实的状态恢复(从 tab.savedState
setTabsState({
...tabsState,
tabs: tabsState.tabs.map((t) => (t.id === tabId ? { ...t, isDormant: false, lastAccessTime: Date.now() } : t))
})
},
[tabsState, setTabsState]
)
const updateTab = useCallback(
(id: string, updates: Partial<Tab>) => {
setTabsState({
...tabsState,
tabs: tabsState.tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
})
},
[tabsState, setTabsState]
)
const setActiveTab = useCallback(
(id: string) => {
if (id === activeTabId) return
const targetTab = tabs.find((t) => t.id === id)
if (!targetTab) return
// 1. 准备更新后的标签列表
let updatedTabs = tabsState.tabs.map((t) =>
t.id === id
? {
...t,
lastAccessTime: Date.now(),
// 如果目标是休眠状态,唤醒它
isDormant: false
}
: t
)
// 2. 如果唤醒了休眠标签,记录日志
if (targetTab.isDormant) {
logger.info('Tab awakened', { tabId: id, route: targetTab.url })
}
// 3. 执行休眠检查(可能需要休眠其他标签)
updatedTabs = performHibernationCheck(updatedTabs, id)
// 4. 更新状态
setTabsState({ tabs: updatedTabs, activeTabId: id })
},
[activeTabId, tabs, tabsState, setTabsState, performHibernationCheck]
)
const addTab = useCallback(
(tab: Tab) => {
const exists = tabs.find((t) => t.id === tab.id)
if (exists) {
setActiveTab(tab.id)
return
}
// 添加 LRU 字段,保留完整 URL含 search/hash
const newTab: Tab = {
...tab,
lastAccessTime: Date.now(),
isDormant: false
}
// 执行休眠检查
let newTabs = [...tabs, newTab]
newTabs = performHibernationCheck(newTabs, tab.id)
setTabsState({ tabs: newTabs, activeTabId: tab.id })
},
[tabs, setTabsState, setActiveTab, performHibernationCheck]
)
const closeTab = useCallback(
(id: string) => {
let newTabs = tabs.filter((t) => t.id !== id)
let newActiveId = activeTabId
if (activeTabId === id) {
const index = tabs.findIndex((t) => t.id === id)
const nextTab = newTabs[index - 1] || newTabs[index]
newActiveId = nextTab ? nextTab.id : ''
if (nextTab?.isDormant) {
newTabs = newTabs.map((t) =>
t.id === nextTab.id ? { ...t, isDormant: false, lastAccessTime: Date.now() } : t
)
}
}
setTabsState({ tabs: newTabs, activeTabId: newActiveId })
},
[tabs, activeTabId, setTabsState]
)
const setTabs = useCallback(
(newTabs: Tab[] | ((prev: Tab[]) => Tab[])) => {
const resolvedTabs = typeof newTabs === 'function' ? newTabs(tabs) : newTabs
setTabsState({ ...tabsState, tabs: resolvedTabs })
},
[tabs, tabsState, setTabsState]
)
/**
* Open a Tab - reuses existing tab or creates new one
*
* @example
* // Basic usage - reuses existing tab if URL matches
* openTab('/settings')
*
* @example
* // With custom title
* openTab('/chat/123', { title: 'Chat with Alice' })
*
* @example
* // Force open new tab (e.g., Cmd+Click)
* openTab('/settings', { forceNew: true })
*
* @example
* // Open webview tab
* openTab('https://example.com', { type: 'webview', title: 'Example' })
*/
const openTab = useCallback(
(url: string, options: OpenTabOptions = {}) => {
const { forceNew = false, title, type = 'route', id } = options
// Try to find existing tab with same URL (unless forceNew)
if (!forceNew) {
const existingTab = tabs.find((t) => t.type === type && t.url === url)
if (existingTab) {
setActiveTab(existingTab.id)
return existingTab.id
}
}
// Create new tab with default route title and LRU fields
const newTab: Tab = {
id: id || uuid(),
type,
url, // full URL including search/hash
title: title || getDefaultRouteTitle(url),
lastAccessTime: Date.now(),
isDormant: false
}
addTab(newTab)
return newTab.id
},
[tabs, setActiveTab, addTab]
)
/**
* Pin a tab (exempt from LRU hibernation)
*/
const pinTab = useCallback(
(id: string) => {
updateTab(id, { isPinned: true })
logger.info('Tab pinned', { tabId: id })
},
[updateTab]
)
/**
* Unpin a tab
*/
const unpinTab = useCallback(
(id: string) => {
updateTab(id, { isPinned: false })
logger.info('Tab unpinned', { tabId: id })
},
[updateTab]
)
/**
* Get the currently active tab
*/
const activeTab = useMemo(() => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId])
return {
// State
tabs,
activeTabId,
activeTab,
isLoading: false,
// Basic operations
addTab,
closeTab,
setActiveTab,
updateTab,
setTabs,
// High-level Tab operations
openTab,
// LRU operations
hibernateTab,
wakeTab,
pinTab,
unpinTab
}
}

View File

@ -4922,6 +4922,9 @@
"title": "Page Zoom"
}
},
"tab": {
"new": "New Tab"
},
"title": {
"apps": "Apps",
"code": "Code",

View File

@ -4922,6 +4922,9 @@
"title": "缩放"
}
},
"tab": {
"new": "新标签页"
},
"title": {
"apps": "小程序",
"code": "Code",

View File

@ -4922,6 +4922,9 @@
"title": "縮放"
}
},
"tab": {
"new": "新標籤頁"
},
"title": {
"apps": "小程式",
"code": "程式碼",

View File

@ -4919,6 +4919,9 @@
"title": "Zoom"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Mini-Apps",
"code": "Code",

View File

@ -4919,6 +4919,9 @@
"title": "Κλίμακα"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Εφαρμογές",
"code": "Κώδικας",

View File

@ -4919,6 +4919,9 @@
"title": "Escala"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Aplicaciones",
"code": "Código",

View File

@ -4919,6 +4919,9 @@
"title": "Zoom"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Mini-programmes",
"code": "Code",

View File

@ -4919,6 +4919,9 @@
"title": "ページズーム"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "アプリ",
"code": "Code",

View File

@ -4919,6 +4919,9 @@
"title": "Escala"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Miniaplicativos",
"code": "Código",

View File

@ -4919,6 +4919,9 @@
"title": "Масштаб страницы"
}
},
"tab": {
"new": "[to be translated]:New Tab"
},
"title": {
"apps": "Приложения",
"code": "Code",

View File

@ -19,12 +19,12 @@ import { getClaudeSupportedProviders } from '@renderer/utils/provider'
import type { TerminalConfig } from '@shared/config/constant'
import { codeTools, terminalApps } from '@shared/config/constant'
import { isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
import { Link } from '@tanstack/react-router'
import { Alert, Checkbox, Input, Popover, Select, Space } from 'antd'
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import {

View File

@ -11,9 +11,9 @@ import { getTopicById } from '@renderer/hooks/useTopic'
import { getAssistantById } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import type { Topic } from '@renderer/types'
import { classNames, runAsyncFunction } from '@renderer/utils'
import { useNavigate } from '@tanstack/react-router'
import { Divider, Empty } from 'antd'
import { t } from 'i18next'
import { Forward } from 'lucide-react'
@ -27,7 +27,8 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
}
const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
const navigate = NavigationService.navigate!
const navigate = useNavigate()
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const [messageStyle] = usePreference('chat.message.style')
const { setTimeoutTimer } = useTimer()
@ -53,7 +54,7 @@ const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
await modelGenerating()
SearchPopup.hide()
const assistant = getAssistantById(topic.assistantId)
navigate('/', { state: { assistant, topic } })
navigate({ to: '/app/chat', search: { assistantId: assistant?.id, topicId: topic.id } })
setTimeoutTimer('onContinueChat', () => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
}

View File

@ -10,11 +10,11 @@ import NavigationService from '@renderer/services/NavigationService'
import { newMessagesActions } from '@renderer/store/newMessage'
import type { Assistant, Topic } from '@renderer/types'
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SECOND_MIN_WINDOW_WIDTH } from '@shared/config/constant'
import { useNavigate, useSearch } from '@tanstack/react-router'
import { AnimatePresence, motion } from 'motion/react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Chat from './Chat'
@ -31,13 +31,19 @@ const HomePage: FC = () => {
// Initialize agent session hook
useAgentSessionInitializer()
const location = useLocation()
const state = location.state
const search = useSearch({ strict: false }) as { assistantId?: string; topicId?: string }
// 根据 search params 中的 ID 查找对应的 assistant
const assistantFromSearch = search.assistantId ? assistants.find((a) => a.id === search.assistantId) : undefined
const [activeAssistant, _setActiveAssistant] = useState<Assistant>(
state?.assistant || _activeAssistant || assistants[0]
assistantFromSearch || _activeAssistant || assistants[0]
)
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', state?.topic)
// 根据 search params 中的 topicId 查找对应的 topic
const topicFromSearch = search.topicId ? activeAssistant?.topics?.find((t) => t.id === search.topicId) : undefined
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id ?? '', topicFromSearch)
const [showAssistants] = usePreference('assistant.tab.show')
const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
@ -79,10 +85,10 @@ const HomePage: FC = () => {
}, [navigate])
useEffect(() => {
state?.assistant && setActiveAssistant(state?.assistant)
state?.topic && setActiveTopic(state?.topic)
assistantFromSearch && setActiveAssistant(assistantFromSearch)
topicFromSearch && setActiveTopic(topicFromSearch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
}, [search.assistantId, search.topicId])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics

View File

@ -5,11 +5,11 @@ import { QuickPanelReservedSymbol, useQuickPanel } from '@renderer/components/Qu
import type { ToolQuickPanelApi } from '@renderer/pages/home/Inputbar/types'
import { useAppSelector } from '@renderer/store'
import type { KnowledgeBase } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import { CircleX, FileSearch, Plus } from 'lucide-react'
import type { FC } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
interface Props {
quickPanel: ToolQuickPanelApi
@ -54,7 +54,7 @@ const KnowledgeBaseButton: FC<Props> = ({ quickPanel, selectedBases, onSelect, d
items.push({
label: t('knowledge.add.title') + '...',
icon: <Plus />,
action: () => navigate('/knowledge'),
action: () => navigate({ to: '/app/knowledge' }),
isSelected: false
})

View File

@ -12,12 +12,12 @@ import { EventEmitter } from '@renderer/services/EventService'
import type { MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { isGeminiWebSearchProvider, isSupportUrlContextProvider } from '@renderer/utils/provider'
import { useNavigate } from '@tanstack/react-router'
import { Form, Input } from 'antd'
import { CircleX, Hammer, Plus } from 'lucide-react'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
interface Props {
assistantId: string
@ -205,7 +205,7 @@ const MCPToolsButton: FC<Props> = ({ quickPanel, setInputValue, resizeTextArea,
newList.push({
label: t('settings.mcp.addServer.label') + '...',
icon: <Plus />,
action: () => navigate('/settings/mcp')
action: () => navigate({ to: '/settings/mcp' })
})
newList.unshift({

View File

@ -9,6 +9,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import type { FileType, Model } from '@renderer/types'
import { FileTypes } from '@renderer/types'
import { getFancyProviderName } from '@renderer/utils'
import { useNavigate } from '@tanstack/react-router'
import { Avatar } from 'antd'
import { useLiveQuery } from 'dexie-react-hooks'
import { first, sortBy } from 'lodash'
@ -16,7 +17,6 @@ import { AtSign, CircleX, Plus } from 'lucide-react'
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
export type MentionTriggerInfo = { type: 'input' | 'button'; position?: number; originalText?: string }
@ -194,7 +194,7 @@ export const useMentionModelsPanel = (params: Params, role: 'button' | 'manager'
items.push({
label: t('settings.models.add.add_model') + '...',
icon: <Plus />,
action: () => navigate('/settings/provider'),
action: () => navigate({ to: '/settings/provider' }),
isSelected: false
})

View File

@ -3,7 +3,6 @@ import CodeViewer from '@renderer/components/CodeViewer'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTimer } from '@renderer/hooks/useTimer'
import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label'
import { getProviderById } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store'
import { removeBlocksThunk } from '@renderer/store/thunk/messageThunk'
import type { SerializedAiSdkError, SerializedAiSdkErrorUnion, SerializedError } from '@renderer/types/error'
@ -35,10 +34,10 @@ import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
import { formatFileSize } from '@renderer/utils/file'
import { KB } from '@shared/config/constant'
import { Link } from '@tanstack/react-router'
import { Alert as AntdAlert, Modal } from 'antd'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
@ -103,11 +102,7 @@ const ErrorMessage: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
values={{ provider: getProviderLabel(providerId) }}
components={{
provider: (
<Link
style={{ color: 'var(--color-link)' }}
to={`/settings/provider`}
state={{ provider: getProviderById(providerId) }}
/>
<Link style={{ color: 'var(--color-link)' }} to="/settings/provider" search={{ id: providerId }} />
)
}}
/>

View File

@ -16,6 +16,7 @@ import styled from 'styled-components'
import AssistantsDrawer from './components/AssistantsDrawer'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
activeTopic: Topic

View File

@ -1,11 +1,11 @@
import App from '@renderer/components/MinApp/MinApp'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useNavigate } from '@tanstack/react-router'
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
const LaunchpadPage: FC = () => {
@ -18,49 +18,49 @@ const LaunchpadPage: FC = () => {
{
icon: <LayoutGrid size={32} className="icon" />,
text: t('title.apps'),
path: '/apps',
path: '/app/minapp',
bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性
},
{
icon: <FileSearch size={32} className="icon" />,
text: t('title.knowledge'),
path: '/knowledge',
path: '/app/knowledge',
bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识
},
{
icon: <Palette size={32} className="icon" />,
text: t('title.paintings'),
path: `/paintings/${defaultPaintingProvider}`,
path: `/app/paintings/${defaultPaintingProvider}`,
bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术
},
{
icon: <Sparkle size={32} className="icon" />,
text: t('title.store'),
path: '/store',
path: '/app/assistant',
bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手靛蓝渐变代表智能和科技
},
{
icon: <Languages size={32} className="icon" />,
text: t('title.translate'),
path: '/translate',
path: '/app/translate',
bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅
},
{
icon: <Folder size={32} className="icon" />,
text: t('title.files'),
path: '/files',
path: '/app/files',
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
},
{
icon: <Code size={32} className="icon" />,
text: t('title.code'),
path: '/code',
path: '/app/code',
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI高级暗黑色代表专业和技术
},
{
icon: <NotepadText size={32} className="icon" />,
text: t('title.notes'),
path: '/notes',
path: '/app/notes',
bgColor: 'linear-gradient(135deg, #F97316, #FB923C)' // 笔记:橙色,代表活力和清晰思路
}
]
@ -87,7 +87,7 @@ const LaunchpadPage: FC = () => {
<SectionTitle>{t('launchpad.apps')}</SectionTitle>
<Grid>
{appMenuItems.map((item) => (
<AppIcon key={item.path} onClick={() => navigate(item.path)}>
<AppIcon key={item.path} onClick={() => navigate({ to: item.path })}>
<IconContainer>
<IconWrapper bgColor={item.bgColor}>{item.icon}</IconWrapper>
</IconContainer>

View File

@ -6,10 +6,10 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import TabsService from '@renderer/services/TabsService'
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import { useNavigate, useParams } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
@ -20,7 +20,7 @@ import WebviewSearch from './components/WebviewSearch'
const logger = loggerService.withContext('MinAppPage')
const MinAppPage: FC = () => {
const { appId } = useParams<{ appId: string }>()
const { appId } = useParams({ strict: false }) as { appId: string }
const { isTopNavbar } = useNavbarPosition()
const { openMinappKeepAlive, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps()
@ -64,7 +64,7 @@ const MinAppPage: FC = () => {
useEffect(() => {
// If app not found, redirect to apps list
if (!app) {
navigate('/apps')
navigate({ to: '/app/minapp' })
return
}
@ -72,7 +72,7 @@ const MinAppPage: FC = () => {
// Only check once and only if we haven't already redirected
if (!initialIsTopNavbar.current && !hasRedirected.current) {
hasRedirected.current = true
navigate('/apps')
navigate({ to: '/app/minapp' })
// Open popup after navigation
setTimeout(() => {
openMinappKeepAlive(app)

View File

@ -15,11 +15,11 @@ import { isDev } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import type { MinAppType } from '@renderer/types'
import { useNavigate } from '@tanstack/react-router'
import type { WebviewTag } from 'electron'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
const logger = loggerService.withContext('MinimalToolbar')
@ -213,7 +213,7 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
}, [app.id, webviewRef, scheduleNavigationUpdate])
const handleMinimize = useCallback(() => {
navigate('/apps')
navigate({ to: '/app/minapp' })
}, [navigate])
const handleTogglePin = useCallback(() => {

View File

@ -19,12 +19,12 @@ import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata } from '@renderer/types'
import type { PaintingAction, PaintingsState } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Input, InputNumber, Radio, Segmented, Select, Slider, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -667,7 +667,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -11,13 +11,13 @@ import { useAllProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager'
import type { FileMetadata } from '@renderer/types'
import { convertToBase64, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import type { DmxapiPainting } from '@types'
import { Input, InputNumber, Segmented, Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { generationModeType } from '../../types'
@ -653,7 +653,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -29,6 +29,7 @@ import type { PaintingAction, PaintingsState } from '@renderer/types'
import type { FileMetadata } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { isNewApiProvider } from '@renderer/utils/provider'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Empty, InputNumber, Segmented, Select, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { RcFile } from 'antd/es/upload'
@ -37,7 +38,6 @@ import type { FC } from 'react'
import React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -438,7 +438,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}
@ -465,7 +465,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
// 当 modelOptions 为空时,引导用户跳转到 Provider 设置页面,新增 image-generation 端点模型
const handleShowAddModelPopup = () => {
navigate(`/settings/provider?id=${newApiProvider.id}`)
navigate({ to: `/settings/provider?id=${newApiProvider.id}` })
}
useEffect(() => {

View File

@ -15,13 +15,13 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata, OvmsPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Avatar, Input, InputNumber, Select, Slider } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -330,7 +330,7 @@ const OvmsPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -5,9 +5,9 @@ import { setDefaultPaintingProvider } from '@renderer/store/settings'
import { updateTab } from '@renderer/store/tabs'
import type { PaintingProvider, SystemProviderId } from '@renderer/types'
import { isNewApiProvider } from '@renderer/utils/provider'
import { useParams } from '@tanstack/react-router'
import type { FC } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
@ -22,8 +22,8 @@ const logger = loggerService.withContext('PaintingsRoutePage')
const BASE_OPTIONS: SystemProviderId[] = ['zhipu', 'aihubmix', 'silicon', 'dmxapi', 'tokenflux', 'ovms']
const PaintingsRoutePage: FC = () => {
const params = useParams()
const provider = params['*']
const params = useParams({ strict: false }) as { _splat?: string }
const provider = params._splat
const dispatch = useAppDispatch()
const providers = useAllProviders()
const [ovmsStatus, setOvmsStatus] = useState<'not-installed' | 'not-running' | 'running'>('not-running')
@ -49,22 +49,34 @@ const PaintingsRoutePage: FC = () => {
}
}, [provider, dispatch, validOptions])
return (
<Routes>
<Route path="*" element={<NewApiPage Options={validOptions} />} />
<Route path="/zhipu" element={<ZhipuPage Options={validOptions} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={validOptions} />} />
<Route path="/silicon" element={<SiliconPage Options={validOptions} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={validOptions} />} />
<Route path="/tokenflux" element={<TokenFluxPage Options={validOptions} />} />
<Route path="/ovms" element={<OvmsPage Options={validOptions} />} />
<Route path="/new-api" element={<NewApiPage Options={validOptions} />} />
{/* new-api family providers are mounted dynamically below */}
{newApiProviders.map((p) => (
<Route key={p.id} path={`/${p.id}`} element={<NewApiPage Options={validOptions} />} />
))}
</Routes>
)
// 根据 provider 渲染对应的页面
const renderPage = () => {
switch (provider) {
case 'zhipu':
return <ZhipuPage Options={validOptions} />
case 'aihubmix':
return <AihubmixPage Options={validOptions} />
case 'silicon':
return <SiliconPage Options={validOptions} />
case 'dmxapi':
return <DmxapiPage Options={validOptions} />
case 'tokenflux':
return <TokenFluxPage Options={validOptions} />
case 'ovms':
return <OvmsPage Options={validOptions} />
case 'new-api':
return <NewApiPage Options={validOptions} />
default:
// 检查是否是 new-api 家族的 provider
if (provider && newApiProviders.some((p) => p.id === provider)) {
return <NewApiPage Options={validOptions} />
}
// 默认页面
return <NewApiPage Options={validOptions} />
}
}
return renderPage()
}
export default PaintingsRoutePage

View File

@ -23,12 +23,12 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { FileMetadata, Painting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Input, InputNumber, Radio, Select, Slider } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -333,7 +333,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -15,12 +15,12 @@ import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import type { TokenFluxPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -268,7 +268,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -12,12 +12,12 @@ import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager'
import { getErrorMessage, uuid } from '@renderer/utils'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { InputNumber, Radio, Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
@ -260,7 +260,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
navigate({ to: '../' + providerId, replace: true })
}
}

View File

@ -15,7 +15,7 @@ export function checkProviderEnabled(provider: Provider, t: TFunction): Promise<
closable: true,
okText: t('common.go_to_settings'),
onOk: () => {
window.navigate?.(`/settings/provider?id=${provider.id}`)
window.navigate?.({ to: `/settings/provider`, search: { id: provider.id } })
reject('Provider disabled')
},
onCancel: () => reject('Provider disabled')

View File

@ -13,6 +13,7 @@ import i18n from '@renderer/i18n'
// import { setUpdateState as setAppUpdateState } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { ThemeMode, UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { Link } from '@tanstack/react-router'
import { Avatar, Progress, Radio, Row, Tag } from 'antd'
import { debounce } from 'lodash'
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
@ -21,7 +22,6 @@ import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '.'

View File

@ -3,11 +3,11 @@ import { Center, ColFlex } from '@cherrystudio/ui'
import { Button } from '@cherrystudio/ui'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { useNavigate } from '@tanstack/react-router'
import { Alert } from 'antd'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
@ -87,7 +87,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
<Button
className="nodrag rounded-full"
variant={installed ? 'default' : 'destructive'}
onClick={() => navigate('/settings/mcp/mcp-install')}
onClick={() => navigate({ to: '/settings/mcp/mcp-install' })}
size="icon">
{installed ? <CheckCircleOutlined /> : <WarningOutlined />}
</Button>

View File

@ -9,12 +9,12 @@ import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust'
import type { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { matchKeywordsInString } from '@renderer/utils/match'
import { useNavigate } from '@tanstack/react-router'
import { Button, Dropdown, Empty } from 'antd'
import { Plus } from 'lucide-react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
@ -115,7 +115,7 @@ const McpServersList: FC = () => {
isActive: false
}
addMCPServer(newServer)
navigate(`/settings/mcp/settings/${encodeURIComponent(newServer.id)}`)
navigate({ to: `/settings/mcp/settings/${encodeURIComponent(newServer.id)}` })
window.toast.success(t('settings.mcp.addSuccess'))
}, [addMCPServer, navigate, t])
@ -260,7 +260,7 @@ const McpServersList: FC = () => {
isLoading={loadingServerIds.has(server.id)}
onToggle={async (active) => await handleToggleActive(server, active)}
onDelete={() => onDeleteMcpServer(server)}
onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}
onEdit={() => navigate({ to: `/settings/mcp/settings/${encodeURIComponent(server.id)}` })}
onOpenUrl={(url) => window.open(url, '_blank')}
/>
)}

View File

@ -12,13 +12,13 @@ import type { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types
import { parseKeyValueString } from '@renderer/utils/env'
import { formatMcpError } from '@renderer/utils/error'
import type { MCPServerLogEntry } from '@shared/config/types'
import { useNavigate, useParams } from '@tanstack/react-router'
import type { TabsProps } from 'antd'
import { Badge, Form, Input, Modal, Radio, Select, Tabs, Tag, Typography } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { ChevronDown, SaveIcon } from 'lucide-react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useParams } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
@ -69,7 +69,8 @@ type TabKey = 'settings' | 'description' | 'tools' | 'prompts' | 'resources'
const McpSettings: React.FC = () => {
const { t } = useTranslation()
const { serverId } = useParams<{ serverId: string }>()
const params = useParams({ strict: false }) as { serverId?: string }
const serverId = params.serverId
const decodedServerId = serverId ? decodeURIComponent(serverId) : ''
const server = useMCPServer(decodedServerId).server as MCPServer
const { deleteMCPServer, updateMCPServer } = useMCPServers()
@ -411,7 +412,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server)
deleteMCPServer(server.id)
window.toast.success(t('settings.mcp.deleteSuccess'))
navigate('/settings/mcp')
navigate({ to: '/settings/mcp' })
}
})
} catch (error: any) {

View File

@ -9,30 +9,17 @@ import DividerWithText from '@renderer/components/DividerWithText'
import { McpLogo } from '@renderer/components/Icons'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { Link, Outlet, useLocation, useNavigate } from '@tanstack/react-router'
import { Button, Flex } from 'antd'
import { FolderCog, Package, ShoppingBag } from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { SettingContainer } from '..'
import BuiltinMCPServerList from './BuiltinMCPServerList'
import InstallNpxUv from './InstallNpxUv'
import McpMarketList from './McpMarketList'
import ProviderDetail from './McpProviderSettings'
import McpServersList from './McpServersList'
import McpSettings from './McpSettings'
import NpxSearch from './NpxSearch'
import { providers } from './providers/config'
const MCPSettings: FC = () => {
const { theme } = useTheme()
const { t } = useTranslation()
const { mcpServers } = useMCPServers()
const navigate = useNavigate()
const location = useLocation()
@ -85,7 +72,7 @@ const MCPSettings: FC = () => {
<ListItem
title={t('settings.mcp.servers', 'MCP Servers')}
active={activeView === 'servers'}
onClick={() => navigate('/settings/mcp/servers')}
onClick={() => navigate({ to: '/settings/mcp/servers' })}
icon={<McpLogo width={18} height={18} style={{ opacity: 0.8 }} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -93,14 +80,14 @@ const MCPSettings: FC = () => {
<ListItem
title={t('settings.mcp.builtinServers', 'Built-in Servers')}
active={activeView === 'builtin'}
onClick={() => navigate('/settings/mcp/builtin')}
onClick={() => navigate({ to: '/settings/mcp/builtin' })}
icon={<Package size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
<ListItem
title={t('settings.mcp.marketplaces', 'Marketplaces')}
active={activeView === 'marketplaces'}
onClick={() => navigate('/settings/mcp/marketplaces')}
onClick={() => navigate({ to: '/settings/mcp/marketplaces' })}
icon={<ShoppingBag size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -110,7 +97,7 @@ const MCPSettings: FC = () => {
key={provider.key}
title={provider.name}
active={activeView === provider.key}
onClick={() => navigate(`/settings/mcp/${provider.key}`)}
onClick={() => navigate({ to: `/settings/mcp/${provider.key}` })}
icon={providerIcons[provider.key] || <FolderCog size={16} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -126,50 +113,7 @@ const MCPSettings: FC = () => {
</Link>
</BackButtonContainer>
)}
<Routes>
<Route index element={<Navigate to="servers" replace />} />
<Route path="servers" element={<McpServersList />} />
<Route path="settings/:serverId" element={<McpSettings />} />
<Route
path="npx-search"
element={
<SettingContainer theme={theme}>
<NpxSearch />
</SettingContainer>
}
/>
<Route
path="mcp-install"
element={
<SettingContainer style={{ backgroundColor: 'inherit' }}>
<InstallNpxUv />
</SettingContainer>
}
/>
<Route
path="builtin"
element={
<ContentWrapper>
<BuiltinMCPServerList />
</ContentWrapper>
}
/>
<Route
path="marketplaces"
element={
<ContentWrapper>
<McpMarketList />
</ContentWrapper>
}
/>
{providers.map((provider) => (
<Route
key={provider.key}
path={provider.key}
element={<ProviderDetail provider={provider} existingServers={mcpServers} />}
/>
))}
</Routes>
<Outlet />
</RightContainer>
</MainContainer>
</Container>
@ -213,12 +157,6 @@ const ProviderIcon = styled.img`
background-color: var(--color-background-soft);
`
const ContentWrapper = styled.div`
padding: 20px;
overflow-y: auto;
height: 100%;
`
const BackButtonContainer = styled.div`
display: flex;
align-items: center;

View File

@ -14,13 +14,13 @@ import ImageStorage from '@renderer/services/ImageStorage'
import type { Provider, ProviderType } from '@renderer/types'
import { isSystemProvider } from '@renderer/types'
import { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils'
import { useLocation, useNavigate, useSearch } from '@tanstack/react-router'
import type { MenuProps } from 'antd'
import { Dropdown, Input, Tag } from 'antd'
import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'react-router-dom'
import styled from 'styled-components'
import useSWRImmutable from 'swr/immutable'
@ -44,7 +44,9 @@ const getIsOvmsSupported = async (): Promise<boolean> => {
}
const ProviderList: FC = () => {
const [searchParams, setSearchParams] = useSearchParams()
const search = useSearch({ strict: false }) as Record<string, string | undefined>
const navigate = useNavigate()
const location = useLocation()
const providers = useAllProviders()
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
const { setTimeoutTimer } = useTimer()
@ -83,8 +85,8 @@ const ProviderList: FC = () => {
}, [providers])
useEffect(() => {
if (searchParams.get('id')) {
const providerId = searchParams.get('id')
if (search.id) {
const providerId = search.id
const provider = providers.find((p) => p.id === providerId)
if (provider) {
setSelectedProvider(provider)
@ -100,10 +102,17 @@ const ProviderList: FC = () => {
} else {
setSelectedProvider(providers[0])
}
searchParams.delete('id')
setSearchParams(searchParams)
// 清除 id 参数
navigate({
to: location.pathname,
search: ({ id, ...rest }) => {
void id
return rest
},
replace: true
})
}
}, [providers, searchParams, setSearchParams, setSelectedProvider, setTimeoutTimer])
}, [providers, search.id, navigate, location.pathname, setSelectedProvider, setTimeoutTimer])
// Handle provider add key from URL schema
useEffect(() => {
@ -117,7 +126,7 @@ const ProviderList: FC = () => {
const { id } = data
const { updatedProvider, isNew, displayName } = await UrlSchemaInfoPopup.show(data)
window.navigate(`/settings/provider?id=${id}`)
navigate({ to: '/settings/provider', search: { id } })
if (!updatedProvider) {
return
@ -134,7 +143,7 @@ const ProviderList: FC = () => {
}
// 检查 URL 参数
const addProviderData = searchParams.get('addProviderData')
const addProviderData = search.addProviderData
if (!addProviderData) {
return
}
@ -143,17 +152,17 @@ const ProviderList: FC = () => {
const { id, apiKey: newApiKey, baseUrl, type, name } = JSON.parse(addProviderData)
if (!id || !newApiKey || !baseUrl) {
window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
window.navigate('/settings/provider')
navigate({ to: '/settings/provider' })
return
}
handleProviderAddKey({ id, apiKey: newApiKey, baseUrl, type, name })
} catch (error) {
window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data'))
window.navigate('/settings/provider')
navigate({ to: '/settings/provider' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams])
}, [search.addProviderData])
const onAddProvider = async () => {
const { name: providerName, type, logo } = await AddProviderPopup.show()

View File

@ -5,12 +5,12 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar'
import type { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preference/preferenceTypes'
import { Link } from '@tanstack/react-router'
import { Radio, Row, Slider } from 'antd'
import { CircleHelp, Edit2 } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import {

View File

@ -1,7 +1,7 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { McpLogo } from '@renderer/components/Icons'
import Scrollbar from '@renderer/components/Scrollbar'
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
import { Link, Outlet, useLocation } from '@tanstack/react-router'
import { Divider as AntDivider } from 'antd'
import {
Brain,
@ -21,26 +21,11 @@ import {
} from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings'
import DocProcessSettings from './DocProcessSettings'
import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
import MemorySettings from './MemorySettings'
import { ProviderList } from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings'
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
import ShortcutSettings from './ShortcutSettings'
import { ApiServerSettings } from './ToolSettings/ApiServerSettings'
import WebSearchSettings from './WebSearchSettings'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
const location = useLocation()
const { pathname } = location
const { t } = useTranslation()
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
@ -148,23 +133,7 @@ const SettingsPage: FC = () => {
</MenuItemLink>
</SettingMenus>
<SettingContent>
<Routes>
<Route path="provider" element={<ProviderList />} />
<Route path="model" element={<ModelSettings />} />
<Route path="websearch/*" element={<WebSearchSettings />} />
<Route path="api-server" element={<ApiServerSettings />} />
<Route path="docprocess" element={<DocProcessSettings />} />
<Route path="quickphrase" element={<QuickPhraseSettings />} />
<Route path="mcp/*" element={<MCPSettings />} />
<Route path="memory" element={<MemorySettings />} />
<Route path="general/*" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />
<Route path="shortcut" element={<ShortcutSettings />} />
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
<Route path="selectionAssistant" element={<SelectionAssistantSettings />} />
<Route path="data" element={<DataSettings />} />
<Route path="about" element={<AboutSettings />} />
</Routes>
<Outlet />
</SettingContent>
</ContentContainer>
</Container>

View File

@ -18,10 +18,10 @@ import { useAppDispatch } from '@renderer/store'
import { setMaxResult, setSearchWithTime } from '@renderer/store/websearch'
import type { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { useNavigate } from '@tanstack/react-router'
import { Slider } from 'antd'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
@ -76,7 +76,7 @@ const BasicSettings: FC = () => {
cancelText: t('common.cancel'),
centered: true,
onOk: () => {
navigate(`/settings/websearch/provider/${provider.id}`)
navigate({ to: '/settings/websearch/provider/$providerId', params: { providerId: provider.id } })
}
})
return

View File

@ -1,13 +1,14 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import type { WebSearchProviderId } from '@renderer/types'
import { useParams } from '@tanstack/react-router'
import type { FC } from 'react'
import { useParams } from 'react-router'
import { SettingContainer, SettingGroup } from '..'
import WebSearchProviderSetting from './WebSearchProviderSetting'
const WebSearchProviderSettings: FC = () => {
const { providerId } = useParams<{ providerId: string }>()
const params = useParams({ strict: false }) as { providerId?: string }
const providerId = params.providerId
const { theme } = useTheme()
if (!providerId) {

View File

@ -12,16 +12,13 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import type { WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Outlet, useLocation, useNavigate } from '@tanstack/react-router'
import { Flex, Tag } from 'antd'
import { Search } from 'lucide-react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
import styled from 'styled-components'
import WebSearchGeneralSettings from './WebSearchGeneralSettings'
import WebSearchProviderSettings from './WebSearchProviderSettings'
const WebSearchSettings: FC = () => {
const { t } = useTranslation()
const { providers } = useWebSearchProviders()
@ -85,7 +82,7 @@ const WebSearchSettings: FC = () => {
<ListItem
title={t('settings.tool.websearch.title')}
active={activeView === 'general'}
onClick={() => navigate('/settings/websearch/general')}
onClick={() => navigate({ to: '/settings/websearch/general' })}
icon={<Search size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
@ -98,7 +95,9 @@ const WebSearchSettings: FC = () => {
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
onClick={() =>
navigate({ to: '/settings/websearch/provider/$providerId', params: { providerId: provider.id } })
}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
@ -128,7 +127,9 @@ const WebSearchSettings: FC = () => {
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
onClick={() =>
navigate({ to: '/settings/websearch/provider/$providerId', params: { providerId: provider.id } })
}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
@ -151,11 +152,7 @@ const WebSearchSettings: FC = () => {
)}
</MenuList>
<RightContainer>
<Routes>
<Route index element={<Navigate to="general" replace />} />
<Route path="general" element={<WebSearchGeneralSettings />} />
<Route path="provider/:providerId" element={<WebSearchProviderSettings />} />
</Routes>
<Outlet />
</RightContainer>
</MainContainer>
</Container>

View File

@ -0,0 +1,981 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as HomeRouteImport } from './routes/home'
import { Route as AppRouteImport } from './routes/app'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SettingsIndexRouteImport } from './routes/settings/index'
import { Route as SettingsWebsearchRouteImport } from './routes/settings/websearch'
import { Route as SettingsShortcutRouteImport } from './routes/settings/shortcut'
import { Route as SettingsSelectionAssistantRouteImport } from './routes/settings/selectionAssistant'
import { Route as SettingsQuickphraseRouteImport } from './routes/settings/quickphrase'
import { Route as SettingsQuickAssistantRouteImport } from './routes/settings/quickAssistant'
import { Route as SettingsProviderRouteImport } from './routes/settings/provider'
import { Route as SettingsNotesRouteImport } from './routes/settings/notes'
import { Route as SettingsModelRouteImport } from './routes/settings/model'
import { Route as SettingsMemoryRouteImport } from './routes/settings/memory'
import { Route as SettingsMcpRouteImport } from './routes/settings/mcp'
import { Route as SettingsGeneralRouteImport } from './routes/settings/general'
import { Route as SettingsDocprocessRouteImport } from './routes/settings/docprocess'
import { Route as SettingsDisplayRouteImport } from './routes/settings/display'
import { Route as SettingsDataRouteImport } from './routes/settings/data'
import { Route as SettingsApiServerRouteImport } from './routes/settings/api-server'
import { Route as SettingsAboutRouteImport } from './routes/settings/about'
import { Route as AppTranslateRouteImport } from './routes/app/translate'
import { Route as AppNotesRouteImport } from './routes/app/notes'
import { Route as AppKnowledgeRouteImport } from './routes/app/knowledge'
import { Route as AppFilesRouteImport } from './routes/app/files'
import { Route as AppCodeRouteImport } from './routes/app/code'
import { Route as AppChatRouteImport } from './routes/app/chat'
import { Route as AppAssistantRouteImport } from './routes/app/assistant'
import { Route as SettingsWebsearchIndexRouteImport } from './routes/settings/websearch/index'
import { Route as SettingsMcpIndexRouteImport } from './routes/settings/mcp/index'
import { Route as AppPaintingsIndexRouteImport } from './routes/app/paintings/index'
import { Route as AppMinappIndexRouteImport } from './routes/app/minapp/index'
import { Route as SettingsWebsearchGeneralRouteImport } from './routes/settings/websearch/general'
import { Route as SettingsMcpServersRouteImport } from './routes/settings/mcp/servers'
import { Route as SettingsMcpNpxSearchRouteImport } from './routes/settings/mcp/npx-search'
import { Route as SettingsMcpMcpInstallRouteImport } from './routes/settings/mcp/mcp-install'
import { Route as SettingsMcpMarketplacesRouteImport } from './routes/settings/mcp/marketplaces'
import { Route as SettingsMcpBuiltinRouteImport } from './routes/settings/mcp/builtin'
import { Route as SettingsMcpSplatRouteImport } from './routes/settings/mcp/$'
import { Route as AppPaintingsSplatRouteImport } from './routes/app/paintings/$'
import { Route as AppMinappAppIdRouteImport } from './routes/app/minapp/$appId'
import { Route as SettingsWebsearchProviderProviderIdRouteImport } from './routes/settings/websearch/provider.$providerId'
import { Route as SettingsMcpSettingsServerIdRouteImport } from './routes/settings/mcp/settings.$serverId'
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => rootRouteImport,
} as any)
const HomeRoute = HomeRouteImport.update({
id: '/home',
path: '/home',
getParentRoute: () => rootRouteImport,
} as any)
const AppRoute = AppRouteImport.update({
id: '/app',
path: '/app',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsIndexRoute = SettingsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsWebsearchRoute = SettingsWebsearchRouteImport.update({
id: '/websearch',
path: '/websearch',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsShortcutRoute = SettingsShortcutRouteImport.update({
id: '/shortcut',
path: '/shortcut',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsSelectionAssistantRoute =
SettingsSelectionAssistantRouteImport.update({
id: '/selectionAssistant',
path: '/selectionAssistant',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsQuickphraseRoute = SettingsQuickphraseRouteImport.update({
id: '/quickphrase',
path: '/quickphrase',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsQuickAssistantRoute = SettingsQuickAssistantRouteImport.update({
id: '/quickAssistant',
path: '/quickAssistant',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsProviderRoute = SettingsProviderRouteImport.update({
id: '/provider',
path: '/provider',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsNotesRoute = SettingsNotesRouteImport.update({
id: '/notes',
path: '/notes',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsModelRoute = SettingsModelRouteImport.update({
id: '/model',
path: '/model',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsMemoryRoute = SettingsMemoryRouteImport.update({
id: '/memory',
path: '/memory',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsMcpRoute = SettingsMcpRouteImport.update({
id: '/mcp',
path: '/mcp',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsGeneralRoute = SettingsGeneralRouteImport.update({
id: '/general',
path: '/general',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDocprocessRoute = SettingsDocprocessRouteImport.update({
id: '/docprocess',
path: '/docprocess',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDisplayRoute = SettingsDisplayRouteImport.update({
id: '/display',
path: '/display',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsDataRoute = SettingsDataRouteImport.update({
id: '/data',
path: '/data',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsApiServerRoute = SettingsApiServerRouteImport.update({
id: '/api-server',
path: '/api-server',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsAboutRoute = SettingsAboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => SettingsRoute,
} as any)
const AppTranslateRoute = AppTranslateRouteImport.update({
id: '/translate',
path: '/translate',
getParentRoute: () => AppRoute,
} as any)
const AppNotesRoute = AppNotesRouteImport.update({
id: '/notes',
path: '/notes',
getParentRoute: () => AppRoute,
} as any)
const AppKnowledgeRoute = AppKnowledgeRouteImport.update({
id: '/knowledge',
path: '/knowledge',
getParentRoute: () => AppRoute,
} as any)
const AppFilesRoute = AppFilesRouteImport.update({
id: '/files',
path: '/files',
getParentRoute: () => AppRoute,
} as any)
const AppCodeRoute = AppCodeRouteImport.update({
id: '/code',
path: '/code',
getParentRoute: () => AppRoute,
} as any)
const AppChatRoute = AppChatRouteImport.update({
id: '/chat',
path: '/chat',
getParentRoute: () => AppRoute,
} as any)
const AppAssistantRoute = AppAssistantRouteImport.update({
id: '/assistant',
path: '/assistant',
getParentRoute: () => AppRoute,
} as any)
const SettingsWebsearchIndexRoute = SettingsWebsearchIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => SettingsWebsearchRoute,
} as any)
const SettingsMcpIndexRoute = SettingsMcpIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => SettingsMcpRoute,
} as any)
const AppPaintingsIndexRoute = AppPaintingsIndexRouteImport.update({
id: '/paintings/',
path: '/paintings/',
getParentRoute: () => AppRoute,
} as any)
const AppMinappIndexRoute = AppMinappIndexRouteImport.update({
id: '/minapp/',
path: '/minapp/',
getParentRoute: () => AppRoute,
} as any)
const SettingsWebsearchGeneralRoute =
SettingsWebsearchGeneralRouteImport.update({
id: '/general',
path: '/general',
getParentRoute: () => SettingsWebsearchRoute,
} as any)
const SettingsMcpServersRoute = SettingsMcpServersRouteImport.update({
id: '/servers',
path: '/servers',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpNpxSearchRoute = SettingsMcpNpxSearchRouteImport.update({
id: '/npx-search',
path: '/npx-search',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpMcpInstallRoute = SettingsMcpMcpInstallRouteImport.update({
id: '/mcp-install',
path: '/mcp-install',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpMarketplacesRoute = SettingsMcpMarketplacesRouteImport.update({
id: '/marketplaces',
path: '/marketplaces',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpBuiltinRoute = SettingsMcpBuiltinRouteImport.update({
id: '/builtin',
path: '/builtin',
getParentRoute: () => SettingsMcpRoute,
} as any)
const SettingsMcpSplatRoute = SettingsMcpSplatRouteImport.update({
id: '/$',
path: '/$',
getParentRoute: () => SettingsMcpRoute,
} as any)
const AppPaintingsSplatRoute = AppPaintingsSplatRouteImport.update({
id: '/paintings/$',
path: '/paintings/$',
getParentRoute: () => AppRoute,
} as any)
const AppMinappAppIdRoute = AppMinappAppIdRouteImport.update({
id: '/minapp/$appId',
path: '/minapp/$appId',
getParentRoute: () => AppRoute,
} as any)
const SettingsWebsearchProviderProviderIdRoute =
SettingsWebsearchProviderProviderIdRouteImport.update({
id: '/provider/$providerId',
path: '/provider/$providerId',
getParentRoute: () => SettingsWebsearchRoute,
} as any)
const SettingsMcpSettingsServerIdRoute =
SettingsMcpSettingsServerIdRouteImport.update({
id: '/settings/$serverId',
path: '/settings/$serverId',
getParentRoute: () => SettingsMcpRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/app': typeof AppRouteWithChildren
'/home': typeof HomeRoute
'/settings': typeof SettingsRouteWithChildren
'/app/assistant': typeof AppAssistantRoute
'/app/chat': typeof AppChatRoute
'/app/code': typeof AppCodeRoute
'/app/files': typeof AppFilesRoute
'/app/knowledge': typeof AppKnowledgeRoute
'/app/notes': typeof AppNotesRoute
'/app/translate': typeof AppTranslateRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/mcp': typeof SettingsMcpRouteWithChildren
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings/websearch': typeof SettingsWebsearchRouteWithChildren
'/settings/': typeof SettingsIndexRoute
'/app/minapp/$appId': typeof AppMinappAppIdRoute
'/app/paintings/$': typeof AppPaintingsSplatRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/websearch/general': typeof SettingsWebsearchGeneralRoute
'/app/minapp': typeof AppMinappIndexRoute
'/app/paintings': typeof AppPaintingsIndexRoute
'/settings/mcp/': typeof SettingsMcpIndexRoute
'/settings/websearch/': typeof SettingsWebsearchIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
'/settings/websearch/provider/$providerId': typeof SettingsWebsearchProviderProviderIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/app': typeof AppRouteWithChildren
'/home': typeof HomeRoute
'/app/assistant': typeof AppAssistantRoute
'/app/chat': typeof AppChatRoute
'/app/code': typeof AppCodeRoute
'/app/files': typeof AppFilesRoute
'/app/knowledge': typeof AppKnowledgeRoute
'/app/notes': typeof AppNotesRoute
'/app/translate': typeof AppTranslateRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings': typeof SettingsIndexRoute
'/app/minapp/$appId': typeof AppMinappAppIdRoute
'/app/paintings/$': typeof AppPaintingsSplatRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/websearch/general': typeof SettingsWebsearchGeneralRoute
'/app/minapp': typeof AppMinappIndexRoute
'/app/paintings': typeof AppPaintingsIndexRoute
'/settings/mcp': typeof SettingsMcpIndexRoute
'/settings/websearch': typeof SettingsWebsearchIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
'/settings/websearch/provider/$providerId': typeof SettingsWebsearchProviderProviderIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/app': typeof AppRouteWithChildren
'/home': typeof HomeRoute
'/settings': typeof SettingsRouteWithChildren
'/app/assistant': typeof AppAssistantRoute
'/app/chat': typeof AppChatRoute
'/app/code': typeof AppCodeRoute
'/app/files': typeof AppFilesRoute
'/app/knowledge': typeof AppKnowledgeRoute
'/app/notes': typeof AppNotesRoute
'/app/translate': typeof AppTranslateRoute
'/settings/about': typeof SettingsAboutRoute
'/settings/api-server': typeof SettingsApiServerRoute
'/settings/data': typeof SettingsDataRoute
'/settings/display': typeof SettingsDisplayRoute
'/settings/docprocess': typeof SettingsDocprocessRoute
'/settings/general': typeof SettingsGeneralRoute
'/settings/mcp': typeof SettingsMcpRouteWithChildren
'/settings/memory': typeof SettingsMemoryRoute
'/settings/model': typeof SettingsModelRoute
'/settings/notes': typeof SettingsNotesRoute
'/settings/provider': typeof SettingsProviderRoute
'/settings/quickAssistant': typeof SettingsQuickAssistantRoute
'/settings/quickphrase': typeof SettingsQuickphraseRoute
'/settings/selectionAssistant': typeof SettingsSelectionAssistantRoute
'/settings/shortcut': typeof SettingsShortcutRoute
'/settings/websearch': typeof SettingsWebsearchRouteWithChildren
'/settings/': typeof SettingsIndexRoute
'/app/minapp/$appId': typeof AppMinappAppIdRoute
'/app/paintings/$': typeof AppPaintingsSplatRoute
'/settings/mcp/$': typeof SettingsMcpSplatRoute
'/settings/mcp/builtin': typeof SettingsMcpBuiltinRoute
'/settings/mcp/marketplaces': typeof SettingsMcpMarketplacesRoute
'/settings/mcp/mcp-install': typeof SettingsMcpMcpInstallRoute
'/settings/mcp/npx-search': typeof SettingsMcpNpxSearchRoute
'/settings/mcp/servers': typeof SettingsMcpServersRoute
'/settings/websearch/general': typeof SettingsWebsearchGeneralRoute
'/app/minapp/': typeof AppMinappIndexRoute
'/app/paintings/': typeof AppPaintingsIndexRoute
'/settings/mcp/': typeof SettingsMcpIndexRoute
'/settings/websearch/': typeof SettingsWebsearchIndexRoute
'/settings/mcp/settings/$serverId': typeof SettingsMcpSettingsServerIdRoute
'/settings/websearch/provider/$providerId': typeof SettingsWebsearchProviderProviderIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/app'
| '/home'
| '/settings'
| '/app/assistant'
| '/app/chat'
| '/app/code'
| '/app/files'
| '/app/knowledge'
| '/app/notes'
| '/app/translate'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/mcp'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings/websearch'
| '/settings/'
| '/app/minapp/$appId'
| '/app/paintings/$'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/websearch/general'
| '/app/minapp'
| '/app/paintings'
| '/settings/mcp/'
| '/settings/websearch/'
| '/settings/mcp/settings/$serverId'
| '/settings/websearch/provider/$providerId'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/app'
| '/home'
| '/app/assistant'
| '/app/chat'
| '/app/code'
| '/app/files'
| '/app/knowledge'
| '/app/notes'
| '/app/translate'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings'
| '/app/minapp/$appId'
| '/app/paintings/$'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/websearch/general'
| '/app/minapp'
| '/app/paintings'
| '/settings/mcp'
| '/settings/websearch'
| '/settings/mcp/settings/$serverId'
| '/settings/websearch/provider/$providerId'
id:
| '__root__'
| '/'
| '/app'
| '/home'
| '/settings'
| '/app/assistant'
| '/app/chat'
| '/app/code'
| '/app/files'
| '/app/knowledge'
| '/app/notes'
| '/app/translate'
| '/settings/about'
| '/settings/api-server'
| '/settings/data'
| '/settings/display'
| '/settings/docprocess'
| '/settings/general'
| '/settings/mcp'
| '/settings/memory'
| '/settings/model'
| '/settings/notes'
| '/settings/provider'
| '/settings/quickAssistant'
| '/settings/quickphrase'
| '/settings/selectionAssistant'
| '/settings/shortcut'
| '/settings/websearch'
| '/settings/'
| '/app/minapp/$appId'
| '/app/paintings/$'
| '/settings/mcp/$'
| '/settings/mcp/builtin'
| '/settings/mcp/marketplaces'
| '/settings/mcp/mcp-install'
| '/settings/mcp/npx-search'
| '/settings/mcp/servers'
| '/settings/websearch/general'
| '/app/minapp/'
| '/app/paintings/'
| '/settings/mcp/'
| '/settings/websearch/'
| '/settings/mcp/settings/$serverId'
| '/settings/websearch/provider/$providerId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AppRoute: typeof AppRouteWithChildren
HomeRoute: typeof HomeRoute
SettingsRoute: typeof SettingsRouteWithChildren
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/settings': {
id: '/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof SettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/home': {
id: '/home'
path: '/home'
fullPath: '/home'
preLoaderRoute: typeof HomeRouteImport
parentRoute: typeof rootRouteImport
}
'/app': {
id: '/app'
path: '/app'
fullPath: '/app'
preLoaderRoute: typeof AppRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/settings/': {
id: '/settings/'
path: '/'
fullPath: '/settings/'
preLoaderRoute: typeof SettingsIndexRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/websearch': {
id: '/settings/websearch'
path: '/websearch'
fullPath: '/settings/websearch'
preLoaderRoute: typeof SettingsWebsearchRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/shortcut': {
id: '/settings/shortcut'
path: '/shortcut'
fullPath: '/settings/shortcut'
preLoaderRoute: typeof SettingsShortcutRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/selectionAssistant': {
id: '/settings/selectionAssistant'
path: '/selectionAssistant'
fullPath: '/settings/selectionAssistant'
preLoaderRoute: typeof SettingsSelectionAssistantRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/quickphrase': {
id: '/settings/quickphrase'
path: '/quickphrase'
fullPath: '/settings/quickphrase'
preLoaderRoute: typeof SettingsQuickphraseRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/quickAssistant': {
id: '/settings/quickAssistant'
path: '/quickAssistant'
fullPath: '/settings/quickAssistant'
preLoaderRoute: typeof SettingsQuickAssistantRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/provider': {
id: '/settings/provider'
path: '/provider'
fullPath: '/settings/provider'
preLoaderRoute: typeof SettingsProviderRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/notes': {
id: '/settings/notes'
path: '/notes'
fullPath: '/settings/notes'
preLoaderRoute: typeof SettingsNotesRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/model': {
id: '/settings/model'
path: '/model'
fullPath: '/settings/model'
preLoaderRoute: typeof SettingsModelRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/memory': {
id: '/settings/memory'
path: '/memory'
fullPath: '/settings/memory'
preLoaderRoute: typeof SettingsMemoryRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/mcp': {
id: '/settings/mcp'
path: '/mcp'
fullPath: '/settings/mcp'
preLoaderRoute: typeof SettingsMcpRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/general': {
id: '/settings/general'
path: '/general'
fullPath: '/settings/general'
preLoaderRoute: typeof SettingsGeneralRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/docprocess': {
id: '/settings/docprocess'
path: '/docprocess'
fullPath: '/settings/docprocess'
preLoaderRoute: typeof SettingsDocprocessRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/display': {
id: '/settings/display'
path: '/display'
fullPath: '/settings/display'
preLoaderRoute: typeof SettingsDisplayRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/data': {
id: '/settings/data'
path: '/data'
fullPath: '/settings/data'
preLoaderRoute: typeof SettingsDataRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/api-server': {
id: '/settings/api-server'
path: '/api-server'
fullPath: '/settings/api-server'
preLoaderRoute: typeof SettingsApiServerRouteImport
parentRoute: typeof SettingsRoute
}
'/settings/about': {
id: '/settings/about'
path: '/about'
fullPath: '/settings/about'
preLoaderRoute: typeof SettingsAboutRouteImport
parentRoute: typeof SettingsRoute
}
'/app/translate': {
id: '/app/translate'
path: '/translate'
fullPath: '/app/translate'
preLoaderRoute: typeof AppTranslateRouteImport
parentRoute: typeof AppRoute
}
'/app/notes': {
id: '/app/notes'
path: '/notes'
fullPath: '/app/notes'
preLoaderRoute: typeof AppNotesRouteImport
parentRoute: typeof AppRoute
}
'/app/knowledge': {
id: '/app/knowledge'
path: '/knowledge'
fullPath: '/app/knowledge'
preLoaderRoute: typeof AppKnowledgeRouteImport
parentRoute: typeof AppRoute
}
'/app/files': {
id: '/app/files'
path: '/files'
fullPath: '/app/files'
preLoaderRoute: typeof AppFilesRouteImport
parentRoute: typeof AppRoute
}
'/app/code': {
id: '/app/code'
path: '/code'
fullPath: '/app/code'
preLoaderRoute: typeof AppCodeRouteImport
parentRoute: typeof AppRoute
}
'/app/chat': {
id: '/app/chat'
path: '/chat'
fullPath: '/app/chat'
preLoaderRoute: typeof AppChatRouteImport
parentRoute: typeof AppRoute
}
'/app/assistant': {
id: '/app/assistant'
path: '/assistant'
fullPath: '/app/assistant'
preLoaderRoute: typeof AppAssistantRouteImport
parentRoute: typeof AppRoute
}
'/settings/websearch/': {
id: '/settings/websearch/'
path: '/'
fullPath: '/settings/websearch/'
preLoaderRoute: typeof SettingsWebsearchIndexRouteImport
parentRoute: typeof SettingsWebsearchRoute
}
'/settings/mcp/': {
id: '/settings/mcp/'
path: '/'
fullPath: '/settings/mcp/'
preLoaderRoute: typeof SettingsMcpIndexRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/app/paintings/': {
id: '/app/paintings/'
path: '/paintings'
fullPath: '/app/paintings'
preLoaderRoute: typeof AppPaintingsIndexRouteImport
parentRoute: typeof AppRoute
}
'/app/minapp/': {
id: '/app/minapp/'
path: '/minapp'
fullPath: '/app/minapp'
preLoaderRoute: typeof AppMinappIndexRouteImport
parentRoute: typeof AppRoute
}
'/settings/websearch/general': {
id: '/settings/websearch/general'
path: '/general'
fullPath: '/settings/websearch/general'
preLoaderRoute: typeof SettingsWebsearchGeneralRouteImport
parentRoute: typeof SettingsWebsearchRoute
}
'/settings/mcp/servers': {
id: '/settings/mcp/servers'
path: '/servers'
fullPath: '/settings/mcp/servers'
preLoaderRoute: typeof SettingsMcpServersRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/npx-search': {
id: '/settings/mcp/npx-search'
path: '/npx-search'
fullPath: '/settings/mcp/npx-search'
preLoaderRoute: typeof SettingsMcpNpxSearchRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/mcp-install': {
id: '/settings/mcp/mcp-install'
path: '/mcp-install'
fullPath: '/settings/mcp/mcp-install'
preLoaderRoute: typeof SettingsMcpMcpInstallRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/marketplaces': {
id: '/settings/mcp/marketplaces'
path: '/marketplaces'
fullPath: '/settings/mcp/marketplaces'
preLoaderRoute: typeof SettingsMcpMarketplacesRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/builtin': {
id: '/settings/mcp/builtin'
path: '/builtin'
fullPath: '/settings/mcp/builtin'
preLoaderRoute: typeof SettingsMcpBuiltinRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/settings/mcp/$': {
id: '/settings/mcp/$'
path: '/$'
fullPath: '/settings/mcp/$'
preLoaderRoute: typeof SettingsMcpSplatRouteImport
parentRoute: typeof SettingsMcpRoute
}
'/app/paintings/$': {
id: '/app/paintings/$'
path: '/paintings/$'
fullPath: '/app/paintings/$'
preLoaderRoute: typeof AppPaintingsSplatRouteImport
parentRoute: typeof AppRoute
}
'/app/minapp/$appId': {
id: '/app/minapp/$appId'
path: '/minapp/$appId'
fullPath: '/app/minapp/$appId'
preLoaderRoute: typeof AppMinappAppIdRouteImport
parentRoute: typeof AppRoute
}
'/settings/websearch/provider/$providerId': {
id: '/settings/websearch/provider/$providerId'
path: '/provider/$providerId'
fullPath: '/settings/websearch/provider/$providerId'
preLoaderRoute: typeof SettingsWebsearchProviderProviderIdRouteImport
parentRoute: typeof SettingsWebsearchRoute
}
'/settings/mcp/settings/$serverId': {
id: '/settings/mcp/settings/$serverId'
path: '/settings/$serverId'
fullPath: '/settings/mcp/settings/$serverId'
preLoaderRoute: typeof SettingsMcpSettingsServerIdRouteImport
parentRoute: typeof SettingsMcpRoute
}
}
}
interface AppRouteChildren {
AppAssistantRoute: typeof AppAssistantRoute
AppChatRoute: typeof AppChatRoute
AppCodeRoute: typeof AppCodeRoute
AppFilesRoute: typeof AppFilesRoute
AppKnowledgeRoute: typeof AppKnowledgeRoute
AppNotesRoute: typeof AppNotesRoute
AppTranslateRoute: typeof AppTranslateRoute
AppMinappAppIdRoute: typeof AppMinappAppIdRoute
AppPaintingsSplatRoute: typeof AppPaintingsSplatRoute
AppMinappIndexRoute: typeof AppMinappIndexRoute
AppPaintingsIndexRoute: typeof AppPaintingsIndexRoute
}
const AppRouteChildren: AppRouteChildren = {
AppAssistantRoute: AppAssistantRoute,
AppChatRoute: AppChatRoute,
AppCodeRoute: AppCodeRoute,
AppFilesRoute: AppFilesRoute,
AppKnowledgeRoute: AppKnowledgeRoute,
AppNotesRoute: AppNotesRoute,
AppTranslateRoute: AppTranslateRoute,
AppMinappAppIdRoute: AppMinappAppIdRoute,
AppPaintingsSplatRoute: AppPaintingsSplatRoute,
AppMinappIndexRoute: AppMinappIndexRoute,
AppPaintingsIndexRoute: AppPaintingsIndexRoute,
}
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
interface SettingsMcpRouteChildren {
SettingsMcpSplatRoute: typeof SettingsMcpSplatRoute
SettingsMcpBuiltinRoute: typeof SettingsMcpBuiltinRoute
SettingsMcpMarketplacesRoute: typeof SettingsMcpMarketplacesRoute
SettingsMcpMcpInstallRoute: typeof SettingsMcpMcpInstallRoute
SettingsMcpNpxSearchRoute: typeof SettingsMcpNpxSearchRoute
SettingsMcpServersRoute: typeof SettingsMcpServersRoute
SettingsMcpIndexRoute: typeof SettingsMcpIndexRoute
SettingsMcpSettingsServerIdRoute: typeof SettingsMcpSettingsServerIdRoute
}
const SettingsMcpRouteChildren: SettingsMcpRouteChildren = {
SettingsMcpSplatRoute: SettingsMcpSplatRoute,
SettingsMcpBuiltinRoute: SettingsMcpBuiltinRoute,
SettingsMcpMarketplacesRoute: SettingsMcpMarketplacesRoute,
SettingsMcpMcpInstallRoute: SettingsMcpMcpInstallRoute,
SettingsMcpNpxSearchRoute: SettingsMcpNpxSearchRoute,
SettingsMcpServersRoute: SettingsMcpServersRoute,
SettingsMcpIndexRoute: SettingsMcpIndexRoute,
SettingsMcpSettingsServerIdRoute: SettingsMcpSettingsServerIdRoute,
}
const SettingsMcpRouteWithChildren = SettingsMcpRoute._addFileChildren(
SettingsMcpRouteChildren,
)
interface SettingsWebsearchRouteChildren {
SettingsWebsearchGeneralRoute: typeof SettingsWebsearchGeneralRoute
SettingsWebsearchIndexRoute: typeof SettingsWebsearchIndexRoute
SettingsWebsearchProviderProviderIdRoute: typeof SettingsWebsearchProviderProviderIdRoute
}
const SettingsWebsearchRouteChildren: SettingsWebsearchRouteChildren = {
SettingsWebsearchGeneralRoute: SettingsWebsearchGeneralRoute,
SettingsWebsearchIndexRoute: SettingsWebsearchIndexRoute,
SettingsWebsearchProviderProviderIdRoute:
SettingsWebsearchProviderProviderIdRoute,
}
const SettingsWebsearchRouteWithChildren =
SettingsWebsearchRoute._addFileChildren(SettingsWebsearchRouteChildren)
interface SettingsRouteChildren {
SettingsAboutRoute: typeof SettingsAboutRoute
SettingsApiServerRoute: typeof SettingsApiServerRoute
SettingsDataRoute: typeof SettingsDataRoute
SettingsDisplayRoute: typeof SettingsDisplayRoute
SettingsDocprocessRoute: typeof SettingsDocprocessRoute
SettingsGeneralRoute: typeof SettingsGeneralRoute
SettingsMcpRoute: typeof SettingsMcpRouteWithChildren
SettingsMemoryRoute: typeof SettingsMemoryRoute
SettingsModelRoute: typeof SettingsModelRoute
SettingsNotesRoute: typeof SettingsNotesRoute
SettingsProviderRoute: typeof SettingsProviderRoute
SettingsQuickAssistantRoute: typeof SettingsQuickAssistantRoute
SettingsQuickphraseRoute: typeof SettingsQuickphraseRoute
SettingsSelectionAssistantRoute: typeof SettingsSelectionAssistantRoute
SettingsShortcutRoute: typeof SettingsShortcutRoute
SettingsWebsearchRoute: typeof SettingsWebsearchRouteWithChildren
SettingsIndexRoute: typeof SettingsIndexRoute
}
const SettingsRouteChildren: SettingsRouteChildren = {
SettingsAboutRoute: SettingsAboutRoute,
SettingsApiServerRoute: SettingsApiServerRoute,
SettingsDataRoute: SettingsDataRoute,
SettingsDisplayRoute: SettingsDisplayRoute,
SettingsDocprocessRoute: SettingsDocprocessRoute,
SettingsGeneralRoute: SettingsGeneralRoute,
SettingsMcpRoute: SettingsMcpRouteWithChildren,
SettingsMemoryRoute: SettingsMemoryRoute,
SettingsModelRoute: SettingsModelRoute,
SettingsNotesRoute: SettingsNotesRoute,
SettingsProviderRoute: SettingsProviderRoute,
SettingsQuickAssistantRoute: SettingsQuickAssistantRoute,
SettingsQuickphraseRoute: SettingsQuickphraseRoute,
SettingsSelectionAssistantRoute: SettingsSelectionAssistantRoute,
SettingsShortcutRoute: SettingsShortcutRoute,
SettingsWebsearchRoute: SettingsWebsearchRouteWithChildren,
SettingsIndexRoute: SettingsIndexRoute,
}
const SettingsRouteWithChildren = SettingsRoute._addFileChildren(
SettingsRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AppRoute: AppRouteWithChildren,
HomeRoute: HomeRoute,
SettingsRoute: SettingsRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@ -0,0 +1,194 @@
# Routing System Developer Guide
This project uses **TanStack Router + Multi MemoryRouter** architecture, where each Tab has its own independent router instance, enabling native KeepAlive behavior.
## Quick Start
### 1. Adding a New Page
Create a file in the `src/renderer/src/routes/` directory:
```typescript
// routes/knowledge.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/knowledge')({
component: KnowledgePage
})
function KnowledgePage() {
return <div>Knowledge Page</div>
}
```
After running `yarn dev`, TanStack Router will automatically update `routeTree.gen.ts`.
### 2. Routes with Parameters
```typescript
// routes/chat/$topicId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/chat/$topicId')({
component: ChatPage
})
function ChatPage() {
const { topicId } = Route.useParams()
return <div>Chat: {topicId}</div>
}
```
### 3. Nested Routes
```text
routes/
├── settings.tsx # /settings (layout)
├── settings/
│ ├── general.tsx # /settings/general
│ └── provider.tsx # /settings/provider
```
```typescript
// routes/settings.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/settings')({
component: SettingsLayout
})
function SettingsLayout() {
return (
<div className="flex">
<aside>Settings Menu</aside>
<main><Outlet /></main>
</div>
)
}
```
## Navigation API
This project provides two navigation methods:
### 1. Tab-Level Navigation - `openTab`
Open a new Tab or switch to an existing Tab using the `useTabs` hook:
```typescript
import { useTabs } from '@renderer/hooks/useTabs'
function MyComponent() {
const { openTab, closeTab } = useTabs()
// Basic usage - reuse existing Tab or create new one
openTab('/settings')
// With title
openTab('/chat/123', { title: 'Chat with Alice' })
// Force new Tab (even if same URL exists)
openTab('/settings', { forceNew: true })
// Open Webview Tab
openTab('https://example.com', {
type: 'webview',
title: 'Example Site'
})
// Close Tab
closeTab(tabId)
}
```
### 2. In-Tab Navigation - `useNavigate`
Navigate within the same Tab (won't create a new Tab) using TanStack Router's `useNavigate`:
```typescript
import { useNavigate } from '@tanstack/react-router'
function SettingsPage() {
const navigate = useNavigate()
// Navigate to sub-page within current Tab
navigate({ to: '/settings/provider' })
// Navigate with parameters
navigate({ to: '/chat/$topicId', params: { topicId: '123' } })
}
```
### Comparison
| Scenario | Method | Result |
|----------|--------|--------|
| Open new feature module | `openTab('/knowledge')` | Creates new Tab |
| Switch sub-page in settings | `navigate({ to: '/settings/provider' })` | Navigates within current Tab |
| Open detail from list | `openTab('/chat/123', { title: '...' })` | Creates new Tab |
| Go back to previous page | `navigate({ to: '..' })` | Goes back within current Tab |
### API Reference
#### `useTabs()` Return Value
| Property/Method | Type | Description |
|-----------------|------|-------------|
| `tabs` | `Tab[]` | List of all Tabs |
| `activeTabId` | `string` | Currently active Tab ID |
| `activeTab` | `Tab \| undefined` | Currently active Tab object |
| `openTab(url, options?)` | `(url: string, options?: OpenTabOptions) => string` | Open Tab, returns Tab ID |
| `closeTab(id)` | `(id: string) => void` | Close specified Tab |
| `setActiveTab(id)` | `(id: string) => void` | Switch to specified Tab |
| `updateTab(id, updates)` | `(id: string, updates: Partial<Tab>) => void` | Update Tab properties |
#### `OpenTabOptions`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `forceNew` | `boolean` | `false` | Force create new Tab |
| `title` | `string` | URL path | Tab title |
| `type` | `'route' \| 'webview'` | `'route'` | Tab type |
| `id` | `string` | Auto-generated | Custom Tab ID |
## Architecture Overview
```text
AppShell
├── Sidebar
├── TabBar
└── Content Area
├── TabRouter #1 (Home)
│ └── Activity(visible) → MemoryRouter → RouterProvider
├── TabRouter #2 (Settings)
│ └── Activity(hidden) → MemoryRouter → RouterProvider
└── WebviewContainer (for webview tabs)
```
- Each Tab has its own independent `MemoryRouter` instance
- Uses React 19 `<Activity>` component to control visibility
- Components are not unmounted on Tab switch, state is fully preserved (KeepAlive)
## File Structure
```text
src/renderer/src/
├── routes/ # Route pages (TanStack Router file-based routing)
│ ├── __root.tsx # Root route (renders Outlet)
│ ├── index.tsx # / Home page
│ ├── settings.tsx # /settings
│ └── README.md # This document
├── components/layout/
│ ├── AppShell.tsx # Main layout (Sidebar + TabBar + Content)
│ └── TabRouter.tsx # Tab router container (MemoryRouter + Activity)
├── hooks/
│ └── useTabs.ts # Tab state management hook
└── routeTree.gen.ts # Auto-generated route tree (do not edit manually)
```
## Important Notes
1. **Do not manually edit `routeTree.gen.ts`** - It is automatically generated by TanStack Router
2. **File name determines route path** - `routes/settings.tsx``/settings`
3. **Dynamic parameters use `$`** - `routes/chat/$topicId.tsx``/chat/:topicId`
4. **Page state is automatically preserved** - Tab switching won't lose `useState`, scroll position, etc.

View File

@ -0,0 +1,194 @@
# 路由系统开发指南
本项目使用 **TanStack Router + Multi MemoryRouter** 架构,每个 Tab 拥有独立的路由实例,实现原生 KeepAlive。
## 快速开始
### 1. 添加新页面
`src/renderer/src/routes/` 目录下创建文件:
```typescript
// routes/knowledge.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/knowledge')({
component: KnowledgePage
})
function KnowledgePage() {
return <div>Knowledge Page</div>
}
```
运行 `yarn dev`TanStack Router 会自动更新 `routeTree.gen.ts`
### 2. 带参数的路由
```typescript
// routes/chat/$topicId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/chat/$topicId')({
component: ChatPage
})
function ChatPage() {
const { topicId } = Route.useParams()
return <div>Chat: {topicId}</div>
}
```
### 3. 嵌套路由
```text
routes/
├── settings.tsx # /settings (布局)
├── settings/
│ ├── general.tsx # /settings/general
│ └── provider.tsx # /settings/provider
```
```typescript
// routes/settings.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/settings')({
component: SettingsLayout
})
function SettingsLayout() {
return (
<div className="flex">
<aside>Settings Menu</aside>
<main><Outlet /></main>
</div>
)
}
```
## 导航 API
本项目有两种导航方式:
### 1. Tab 级别导航 - `openTab`
打开新 Tab 或切换到已有 Tab使用 `useTabs` hook
```typescript
import { useTabs } from '@renderer/hooks/useTabs'
function MyComponent() {
const { openTab, closeTab } = useTabs()
// 基础用法 - 复用已有 Tab 或新建
openTab('/settings')
// 带标题
openTab('/chat/123', { title: 'Chat with Alice' })
// 强制新开 Tab即使已有相同 URL
openTab('/settings', { forceNew: true })
// 打开 Webview Tab
openTab('https://example.com', {
type: 'webview',
title: 'Example Site'
})
// 关闭 Tab
closeTab(tabId)
}
```
### 2. Tab 内部导航 - `useNavigate`
在同一个 Tab 内跳转路由(不会新开 Tab使用 TanStack Router 的 `useNavigate`
```typescript
import { useNavigate } from '@tanstack/react-router'
function SettingsPage() {
const navigate = useNavigate()
// 在当前 Tab 内跳转到子页面
navigate({ to: '/settings/provider' })
// 带参数跳转
navigate({ to: '/chat/$topicId', params: { topicId: '123' } })
}
```
### 两者区别
| 场景 | 使用 | 效果 |
|-----|------|------|
| 打开新功能模块 | `openTab('/knowledge')` | 新建 Tab |
| 设置页内切换子页 | `navigate({ to: '/settings/provider' })` | 当前 Tab 内跳转 |
| 从列表打开详情 | `openTab('/chat/123', { title: '...' })` | 新建 Tab |
| 返回上一页 | `navigate({ to: '..' })` | 当前 Tab 内返回 |
### API 参考
#### `useTabs()` 返回值
| 属性/方法 | 类型 | 说明 |
|----------|------|------|
| `tabs` | `Tab[]` | 所有 Tab 列表 |
| `activeTabId` | `string` | 当前激活的 Tab ID |
| `activeTab` | `Tab \| undefined` | 当前激活的 Tab 对象 |
| `openTab(url, options?)` | `(url: string, options?: OpenTabOptions) => string` | 打开 Tab返回 Tab ID |
| `closeTab(id)` | `(id: string) => void` | 关闭指定 Tab |
| `setActiveTab(id)` | `(id: string) => void` | 切换到指定 Tab |
| `updateTab(id, updates)` | `(id: string, updates: Partial<Tab>) => void` | 更新 Tab 属性 |
#### `OpenTabOptions`
| 选项 | 类型 | 默认值 | 说明 |
|-----|------|-------|------|
| `forceNew` | `boolean` | `false` | 强制新开 Tab |
| `title` | `string` | URL 路径 | Tab 标题 |
| `type` | `'route' \| 'webview'` | `'route'` | Tab 类型 |
| `id` | `string` | 自动生成 | 自定义 Tab ID |
## 架构说明
```text
AppShell
├── Sidebar
├── TabBar
└── Content Area
├── TabRouter #1 (Home)
│ └── Activity(visible) → MemoryRouter → RouterProvider
├── TabRouter #2 (Settings)
│ └── Activity(hidden) → MemoryRouter → RouterProvider
└── WebviewContainer (for webview tabs)
```
- 每个 Tab 拥有独立的 `MemoryRouter` 实例
- 使用 React 19 `<Activity>` 组件控制可见性
- Tab 切换时组件不卸载状态完全保持KeepAlive
## 文件结构
```text
src/renderer/src/
├── routes/ # 路由页面TanStack Router 文件路由)
│ ├── __root.tsx # 根路由(渲染 Outlet
│ ├── index.tsx # / 首页
│ ├── settings.tsx # /settings
│ └── README.md # 本文档
├── components/layout/
│ ├── AppShell.tsx # 主布局Sidebar + TabBar + Content
│ └── TabRouter.tsx # Tab 路由容器MemoryRouter + Activity
├── hooks/
│ └── useTabs.ts # Tab 状态管理 Hook
└── routeTree.gen.ts # 自动生成的路由树(勿手动编辑)
```
## 注意事项
1. **不要手动编辑 `routeTree.gen.ts`** - 它由 TanStack Router 自动生成
2. **路由文件命名即路径** - `routes/settings.tsx``/settings`
3. **动态参数使用 `$`** - `routes/chat/$topicId.tsx``/chat/:topicId`
4. **页面状态自动保持** - Tab 切换不会丢失 `useState`、滚动位置等

View File

@ -0,0 +1,11 @@
import NavigationHandler from '@renderer/handler/NavigationHandler'
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<NavigationHandler />
<Outlet />
</>
)
})

View File

@ -0,0 +1,5 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/app')({
component: () => <Outlet />
})

View File

@ -0,0 +1,6 @@
import AssistantPresetsPage from '@renderer/pages/store/assistants/presets/AssistantPresetsPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/assistant')({
component: AssistantPresetsPage
})

View File

@ -0,0 +1,6 @@
import HomePage from '@renderer/pages/home/HomePage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/chat')({
component: HomePage
})

View File

@ -0,0 +1,6 @@
import CodeToolsPage from '@renderer/pages/code/CodeToolsPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/code')({
component: CodeToolsPage
})

View File

@ -0,0 +1,6 @@
import FilesPage from '@renderer/pages/files/FilesPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/files')({
component: FilesPage
})

View File

@ -0,0 +1,6 @@
import KnowledgePage from '@renderer/pages/knowledge/KnowledgePage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/knowledge')({
component: KnowledgePage
})

View File

@ -0,0 +1,6 @@
import MinAppPage from '@renderer/pages/minapps/MinAppPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/minapp/$appId')({
component: MinAppPage
})

View File

@ -0,0 +1,6 @@
import MinAppsPage from '@renderer/pages/minapps/MinAppsPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/minapp/')({
component: MinAppsPage
})

View File

@ -0,0 +1,6 @@
import NotesPage from '@renderer/pages/notes/NotesPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/notes')({
component: NotesPage
})

View File

@ -0,0 +1,7 @@
import PaintingsRoutePage from '@renderer/pages/paintings/PaintingsRoutePage'
import { createFileRoute } from '@tanstack/react-router'
// 通配符路由:捕获 /app/paintings/* 所有子路径
export const Route = createFileRoute('/app/paintings/$')({
component: PaintingsRoutePage
})

View File

@ -0,0 +1,6 @@
import PaintingsRoutePage from '@renderer/pages/paintings/PaintingsRoutePage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/paintings/')({
component: PaintingsRoutePage
})

View File

@ -0,0 +1,6 @@
import TranslatePage from '@renderer/pages/translate/TranslatePage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app/translate')({
component: TranslatePage
})

View File

@ -0,0 +1,6 @@
import LaunchpadPage from '@renderer/pages/launchpad/LaunchpadPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/home')({
component: LaunchpadPage
})

View File

@ -0,0 +1,7 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
beforeLoad: () => {
throw redirect({ to: '/home' })
}
})

View File

@ -0,0 +1,7 @@
import SettingsPage from '@renderer/pages/settings/SettingsPage'
import { createFileRoute } from '@tanstack/react-router'
// 布局路由SettingsPage 作为布局组件,使用 Outlet 渲染子路由
export const Route = createFileRoute('/settings')({
component: SettingsPage
})

View File

@ -0,0 +1,6 @@
import AboutSettings from '@renderer/pages/settings/AboutSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/about')({
component: AboutSettings
})

View File

@ -0,0 +1,6 @@
import { ApiServerSettings } from '@renderer/pages/settings/ToolSettings/ApiServerSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/api-server')({
component: ApiServerSettings
})

View File

@ -0,0 +1,6 @@
import DataSettings from '@renderer/pages/settings/DataSettings/DataSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/data')({
component: DataSettings
})

View File

@ -0,0 +1,6 @@
import DisplaySettings from '@renderer/pages/settings/DisplaySettings/DisplaySettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/display')({
component: DisplaySettings
})

View File

@ -0,0 +1,6 @@
import DocProcessSettings from '@renderer/pages/settings/DocProcessSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/docprocess')({
component: DocProcessSettings
})

View File

@ -0,0 +1,6 @@
import GeneralSettings from '@renderer/pages/settings/GeneralSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/general')({
component: GeneralSettings
})

View File

@ -0,0 +1,8 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
// /settings/ 重定向到 /settings/provider
export const Route = createFileRoute('/settings/')({
beforeLoad: () => {
throw redirect({ to: '/settings/provider' })
}
})

View File

@ -0,0 +1,7 @@
import MCPSettings from '@renderer/pages/settings/MCPSettings'
import { createFileRoute } from '@tanstack/react-router'
// MCP 布局路由MCPSettings 作为布局组件,使用 Outlet 渲染子路由
export const Route = createFileRoute('/settings/mcp')({
component: MCPSettings
})

View File

@ -0,0 +1,24 @@
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import ProviderDetail from '@renderer/pages/settings/MCPSettings/McpProviderSettings'
import { providers } from '@renderer/pages/settings/MCPSettings/providers/config'
import { useParams } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router'
// 通配符路由:捕获 provider 页面 /settings/mcp/:providerKey
const ProviderPage = () => {
const params = useParams({ strict: false }) as { _splat?: string }
const providerKey = params._splat
const { mcpServers } = useMCPServers()
const provider = providers.find((p) => p.key === providerKey)
if (!provider) {
return <div>Provider not found</div>
}
return <ProviderDetail provider={provider} existingServers={mcpServers} />
}
export const Route = createFileRoute('/settings/mcp/$')({
component: ProviderPage
})

View File

@ -0,0 +1,12 @@
import BuiltinMCPServerList from '@renderer/pages/settings/MCPSettings/BuiltinMCPServerList'
import { createFileRoute } from '@tanstack/react-router'
const BuiltinWrapper = () => (
<div className="h-full overflow-y-auto p-5">
<BuiltinMCPServerList />
</div>
)
export const Route = createFileRoute('/settings/mcp/builtin')({
component: BuiltinWrapper
})

View File

@ -0,0 +1,8 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
// /settings/mcp/ 重定向到 /settings/mcp/servers
export const Route = createFileRoute('/settings/mcp/')({
beforeLoad: () => {
throw redirect({ to: '/settings/mcp/servers' })
}
})

View File

@ -0,0 +1,12 @@
import McpMarketList from '@renderer/pages/settings/MCPSettings/McpMarketList'
import { createFileRoute } from '@tanstack/react-router'
const MarketplacesWrapper = () => (
<div className="h-full overflow-y-auto p-5">
<McpMarketList />
</div>
)
export const Route = createFileRoute('/settings/mcp/marketplaces')({
component: MarketplacesWrapper
})

View File

@ -0,0 +1,13 @@
import { SettingContainer } from '@renderer/pages/settings'
import InstallNpxUv from '@renderer/pages/settings/MCPSettings/InstallNpxUv'
import { createFileRoute } from '@tanstack/react-router'
const McpInstallWrapper = () => (
<SettingContainer style={{ backgroundColor: 'inherit' }}>
<InstallNpxUv />
</SettingContainer>
)
export const Route = createFileRoute('/settings/mcp/mcp-install')({
component: McpInstallWrapper
})

View File

@ -0,0 +1,17 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { SettingContainer } from '@renderer/pages/settings'
import NpxSearch from '@renderer/pages/settings/MCPSettings/NpxSearch'
import { createFileRoute } from '@tanstack/react-router'
const NpxSearchWrapper = () => {
const { theme } = useTheme()
return (
<SettingContainer theme={theme}>
<NpxSearch />
</SettingContainer>
)
}
export const Route = createFileRoute('/settings/mcp/npx-search')({
component: NpxSearchWrapper
})

View File

@ -0,0 +1,6 @@
import McpServersList from '@renderer/pages/settings/MCPSettings/McpServersList'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/mcp/servers')({
component: McpServersList
})

View File

@ -0,0 +1,6 @@
import McpSettings from '@renderer/pages/settings/MCPSettings/McpSettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/mcp/settings/$serverId')({
component: McpSettings
})

View File

@ -0,0 +1,6 @@
import MemorySettings from '@renderer/pages/settings/MemorySettings'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/settings/memory')({
component: MemorySettings
})

Some files were not shown because too many files have changed in this diff Show More