diff --git a/biome.jsonc b/biome.jsonc index f7c86da751..6fcb964199 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -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": { diff --git a/electron.vite.config.ts b/electron.vite.config.ts index a6d45b53bd..a59b2b0c20 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -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 diff --git a/eslint.config.mjs b/eslint.config.mjs index 2e6a2cc311..cde667d167 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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' ] }, diff --git a/package.json b/package.json index 9c35419881..fe56334376 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/shared/data/cache/cacheSchemas.ts b/packages/shared/data/cache/cacheSchemas.ts index dc182a9c7e..5c411a172c 100644 --- a/packages/shared/data/cache/cacheSchemas.ts +++ b/packages/shared/data/cache/cacheSchemas.ts @@ -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: '' } } // ============================================================================ diff --git a/packages/shared/data/cache/cacheValueTypes.ts b/packages/shared/data/cache/cacheValueTypes.ts index a012241615..833bca6f33 100644 --- a/packages/shared/data/cache/cacheValueTypes.ts +++ b/packages/shared/data/cache/cacheValueTypes.ts @@ -17,3 +17,38 @@ export type CacheActiveSearches = Record // 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 + // LRU 字段 + lastAccessTime?: number // open/switch 时更新 + isDormant?: boolean // 是否已休眠 + isPinned?: boolean // 是否置顶(豁免 LRU) + savedState?: TabSavedState // 休眠前保存的状态 +} + +export interface TabsState { + tabs: Tab[] + activeTabId: string +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1080a5c62..b9588ed80f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index fb34a13a26..c3d6fd9d7e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 { - + diff --git a/src/renderer/src/Router.tsx b/src/renderer/src/Router.tsx deleted file mode 100644 index fb555d8bc3..0000000000 --- a/src/renderer/src/Router.tsx +++ /dev/null @@ -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 ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ) - }, []) - - if (navbarPosition === 'left') { - return ( - - - {routes} - - - ) - } - - return ( - - - {routes} - - ) -} - -export default Router diff --git a/src/renderer/src/components/FreeTrialModelTag.tsx b/src/renderer/src/components/FreeTrialModelTag.tsx index ad142ae0cf..649452f3de 100644 --- a/src/renderer/src/components/FreeTrialModelTag.tsx +++ b/src/renderer/src/components/FreeTrialModelTag.tsx @@ -30,13 +30,13 @@ export const FreeTrialModelTag: FC = ({ 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) { diff --git a/src/renderer/src/components/MinApp/MinApp.tsx b/src/renderer/src/components/MinApp/MinApp.tsx index 801db2b082..d4da03c945 100644 --- a/src/renderer/src/components/MinApp/MinApp.tsx +++ b/src/renderer/src/components/MinApp/MinApp.tsx @@ -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 = ({ 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) diff --git a/src/renderer/src/components/MinApp/MinAppTabsPool.tsx b/src/renderer/src/components/MinApp/MinAppTabsPool.tsx index 1bef9a532a..26594ad08d 100644 --- a/src/renderer/src/components/MinApp/MinAppTabsPool.tsx +++ b/src/renderer/src/components/MinApp/MinAppTabsPool.tsx @@ -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', '', ...] - 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', '', ...] + return parts.length >= 3 })() const shouldShow = isTopNavbar && isAppDetail diff --git a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx index 1aa7859a99..908f471819 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx @@ -193,7 +193,7 @@ const PopupContainer: React.FC = ({ 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 } }) }} /> diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 82480d7f3d..2e2a959e7e 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -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 = ({ 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]) diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 7794ac3a71..b6b69f190d 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -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 ( { hideMinappPopup() - await modelGenerating() - navigate(path) + await to(path) }}> {iconMap[icon]} diff --git a/src/renderer/src/components/layout/AppShell.tsx b/src/renderer/src/components/layout/AppShell.tsx new file mode 100644 index 0000000000..ddc9198328 --- /dev/null +++ b/src/renderer/src/components/layout/AppShell.tsx @@ -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 }) => ( + +
+
Webview App
+ {url} +
+
+) + +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 ( +
+ {/* Zone 1: Sidebar */} + + +
+ {/* Zone 2: Tab Bar */} + +
+ + {tabs.map((tab) => ( + + {/* TODO: pin功能,形式还未确定 */} + {tab.title} + {tabs.length > 1 && ( +
{ + 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"> + +
+ )} +
+ ))} + {/* 新增 Tab 按钮 - 跟随最后一个 Tab */} + +
+
+
+ + {/* Zone 3: Content Area - Multi MemoryRouter Architecture */} +
+ {/* Route Tabs: Only render non-dormant tabs */} + {tabs + .filter((t) => t.type === 'route' && !t.isDormant) + .map((tab) => ( + handleUrlChange(tab.id, url)} + /> + ))} + + {/* Webview Tabs: Only render non-dormant tabs */} + {tabs + .filter((t) => t.type === 'webview' && !t.isDormant) + .map((tab) => ( + + ))} +
+
+
+ ) +} diff --git a/src/renderer/src/components/layout/TabRouter.tsx b/src/renderer/src/components/layout/TabRouter.tsx new file mode 100644 index 0000000000..9c2f5df258 --- /dev/null +++ b/src/renderer/src/components/layout/TabRouter.tsx @@ -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 ( + +
+ +
+
+ ) +} diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index f4452a5c31..8ff317bf1b 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -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 toast: ToastUtilities agentTools: { respondToPermission: (payload: { diff --git a/src/renderer/src/handler/NavigationHandler.tsx b/src/renderer/src/handler/NavigationHandler.tsx index 5e1ef56113..578f0daab7 100644 --- a/src/renderer/src/handler/NavigationHandler.tsx +++ b/src/renderer/src/handler/NavigationHandler.tsx @@ -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) diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index c847c3b7d8..7d7d6730d7 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -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 }) } }) }, []) diff --git a/src/renderer/src/hooks/useMCPServers.ts b/src/renderer/src/hooks/useMCPServers.ts index 26731e14fd..d9f59ca1a1 100644 --- a/src/renderer/src/hooks/useMCPServers.ts +++ b/src/renderer/src/hooks/useMCPServers.ts @@ -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 diff --git a/src/renderer/src/hooks/useMinappPopup.ts b/src/renderer/src/hooks/useMinappPopup.ts index f08924bd0a..ccf06baba5 100644 --- a/src/renderer/src/hooks/useMinappPopup.ts +++ b/src/renderer/src/hooks/useMinappPopup.ts @@ -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 diff --git a/src/renderer/src/hooks/useTabs.ts b/src/renderer/src/hooks/useTabs.ts new file mode 100644 index 0000000000..5f02d5ac52 --- /dev/null +++ b/src/renderer/src/hooks/useTabs.ts @@ -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(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) => { + 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 + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7ac425c54a..c205027f3a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4922,6 +4922,9 @@ "title": "Page Zoom" } }, + "tab": { + "new": "New Tab" + }, "title": { "apps": "Apps", "code": "Code", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e598016fe2..9989761252 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4922,6 +4922,9 @@ "title": "缩放" } }, + "tab": { + "new": "新标签页" + }, "title": { "apps": "小程序", "code": "Code", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ec39a08058..a9824bf6c9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4922,6 +4922,9 @@ "title": "縮放" } }, + "tab": { + "new": "新標籤頁" + }, "title": { "apps": "小程式", "code": "程式碼", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index d5445961e5..b12015139a 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -4919,6 +4919,9 @@ "title": "Zoom" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Mini-Apps", "code": "Code", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index d0cfe0579c..5fcae03612 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -4919,6 +4919,9 @@ "title": "Κλίμακα" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Εφαρμογές", "code": "Κώδικας", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 039a289e7a..f8bc54e1d3 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -4919,6 +4919,9 @@ "title": "Escala" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Aplicaciones", "code": "Código", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 352678c4ad..57f644f581 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -4919,6 +4919,9 @@ "title": "Zoom" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Mini-programmes", "code": "Code", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index b58fe588f6..fd9667b2dd 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -4919,6 +4919,9 @@ "title": "ページズーム" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "アプリ", "code": "Code", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 24a38261ca..803334d832 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -4919,6 +4919,9 @@ "title": "Escala" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Miniaplicativos", "code": "Código", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 74ce3df5fb..66651e7cb7 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -4919,6 +4919,9 @@ "title": "Масштаб страницы" } }, + "tab": { + "new": "[to be translated]:New Tab" + }, "title": { "apps": "Приложения", "code": "Code", diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index 1e90e8cf57..4944367874 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -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 { diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 3d1400e7d4..a0569288d8 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -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 { } const TopicMessages: FC = ({ 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 = ({ 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) } diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index f0baad3a7f..907b1573de 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -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( - 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 diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/KnowledgeBaseButton.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/KnowledgeBaseButton.tsx index 7c504faa79..74135ea42e 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/KnowledgeBaseButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/KnowledgeBaseButton.tsx @@ -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 = ({ quickPanel, selectedBases, onSelect, d items.push({ label: t('knowledge.add.title') + '...', icon: , - action: () => navigate('/knowledge'), + action: () => navigate({ to: '/app/knowledge' }), isSelected: false }) diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/MCPToolsButton.tsx index 174a96936f..42eec3acd2 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/MCPToolsButton.tsx @@ -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 = ({ quickPanel, setInputValue, resizeTextArea, newList.push({ label: t('settings.mcp.addServer.label') + '...', icon: , - action: () => navigate('/settings/mcp') + action: () => navigate({ to: '/settings/mcp' }) }) newList.unshift({ diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/useMentionModelsPanel.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/useMentionModelsPanel.tsx index cda8816c6b..aa496e5265 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/useMentionModelsPanel.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/useMentionModelsPanel.tsx @@ -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: , - action: () => navigate('/settings/provider'), + action: () => navigate({ to: '/settings/provider' }), isSelected: false }) diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index a07a69b85a..772fbd01fa 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -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: ( - + ) }} /> diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index fc3e1a76a1..20479478a6 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -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 diff --git a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx index f9c80f6aec..f8408f0f9b 100644 --- a/src/renderer/src/pages/launchpad/LaunchpadPage.tsx +++ b/src/renderer/src/pages/launchpad/LaunchpadPage.tsx @@ -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: , text: t('title.apps'), - path: '/apps', + path: '/app/minapp', bgColor: 'linear-gradient(135deg, #8B5CF6, #A855F7)' // 小程序:紫色,代表多功能和灵活性 }, { icon: , text: t('title.knowledge'), - path: '/knowledge', + path: '/app/knowledge', bgColor: 'linear-gradient(135deg, #10B981, #34D399)' // 知识库:翠绿色,代表生长和知识 }, { icon: , text: t('title.paintings'), - path: `/paintings/${defaultPaintingProvider}`, + path: `/app/paintings/${defaultPaintingProvider}`, bgColor: 'linear-gradient(135deg, #EC4899, #F472B6)' // 绘画:活力粉色,代表创造力和艺术 }, { icon: , text: t('title.store'), - path: '/store', + path: '/app/assistant', bgColor: 'linear-gradient(135deg, #6366F1, #4F46E5)' // AI助手:靛蓝渐变,代表智能和科技 }, { icon: , text: t('title.translate'), - path: '/translate', + path: '/app/translate', bgColor: 'linear-gradient(135deg, #06B6D4, #0EA5E9)' // 翻译:明亮的青蓝色,代表沟通和流畅 }, { icon: , text: t('title.files'), - path: '/files', + path: '/app/files', bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性 }, { icon: , text: t('title.code'), - path: '/code', + path: '/app/code', bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI:高级暗黑色,代表专业和技术 }, { icon: , text: t('title.notes'), - path: '/notes', + path: '/app/notes', bgColor: 'linear-gradient(135deg, #F97316, #FB923C)' // 笔记:橙色,代表活力和清晰思路 } ] @@ -87,7 +87,7 @@ const LaunchpadPage: FC = () => { {t('launchpad.apps')} {appMenuItems.map((item) => ( - navigate(item.path)}> + navigate({ to: item.path })}> {item.icon} diff --git a/src/renderer/src/pages/minapps/MinAppPage.tsx b/src/renderer/src/pages/minapps/MinAppPage.tsx index 4ee4549303..b7ac3bd6fe 100644 --- a/src/renderer/src/pages/minapps/MinAppPage.tsx +++ b/src/renderer/src/pages/minapps/MinAppPage.tsx @@ -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) diff --git a/src/renderer/src/pages/minapps/components/MinimalToolbar.tsx b/src/renderer/src/pages/minapps/components/MinimalToolbar.tsx index 0d2d555e52..18d3d68a04 100644 --- a/src/renderer/src/pages/minapps/components/MinimalToolbar.tsx +++ b/src/renderer/src/pages/minapps/components/MinimalToolbar.tsx @@ -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 = ({ app, webviewRef, currentUrl, onReload, onOp }, [app.id, webviewRef, scheduleNavigationUpdate]) const handleMinimize = useCallback(() => { - navigate('/apps') + navigate({ to: '/app/minapp' }) }, [navigate]) const handleTogglePin = useCallback(() => { diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index 294bed47f2..e3b494084d 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 30f582b64e..e691bb63e5 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/NewApiPage.tsx b/src/renderer/src/pages/paintings/NewApiPage.tsx index 26f0bd7b00..effc88efe3 100644 --- a/src/renderer/src/pages/paintings/NewApiPage.tsx +++ b/src/renderer/src/pages/paintings/NewApiPage.tsx @@ -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(() => { diff --git a/src/renderer/src/pages/paintings/OvmsPage.tsx b/src/renderer/src/pages/paintings/OvmsPage.tsx index 5e23c89984..38e207841c 100644 --- a/src/renderer/src/pages/paintings/OvmsPage.tsx +++ b/src/renderer/src/pages/paintings/OvmsPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx b/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx index 6629946879..7f2ce4c781 100644 --- a/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx +++ b/src/renderer/src/pages/paintings/PaintingsRoutePage.tsx @@ -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 ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* new-api family providers are mounted dynamically below */} - {newApiProviders.map((p) => ( - } /> - ))} - - ) + // 根据 provider 渲染对应的页面 + const renderPage = () => { + switch (provider) { + case 'zhipu': + return + case 'aihubmix': + return + case 'silicon': + return + case 'dmxapi': + return + case 'tokenflux': + return + case 'ovms': + return + case 'new-api': + return + default: + // 检查是否是 new-api 家族的 provider + if (provider && newApiProviders.some((p) => p.id === provider)) { + return + } + // 默认页面 + return + } + } + + return renderPage() } export default PaintingsRoutePage diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 16ff35363a..4bd2b85b5d 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/TokenFluxPage.tsx b/src/renderer/src/pages/paintings/TokenFluxPage.tsx index 5ecba7a9b5..9c3f3c4b5b 100644 --- a/src/renderer/src/pages/paintings/TokenFluxPage.tsx +++ b/src/renderer/src/pages/paintings/TokenFluxPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/ZhipuPage.tsx b/src/renderer/src/pages/paintings/ZhipuPage.tsx index 846e0e4e39..7c8e9c5e4c 100644 --- a/src/renderer/src/pages/paintings/ZhipuPage.tsx +++ b/src/renderer/src/pages/paintings/ZhipuPage.tsx @@ -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 }) } } diff --git a/src/renderer/src/pages/paintings/utils/index.ts b/src/renderer/src/pages/paintings/utils/index.ts index 8df6108c1e..d7f7359def 100644 --- a/src/renderer/src/pages/paintings/utils/index.ts +++ b/src/renderer/src/pages/paintings/utils/index.ts @@ -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') diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index 56ac2374d3..783cddc6d9 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -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 '.' diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx index 20d6864582..6bd9acd5d7 100644 --- a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx @@ -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 = ({ mini = false }) => { diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index 436f051faa..2fe6bd3185 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -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')} /> )} diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 4406e0f8ab..aa87476bb4 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -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) { diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 5c11b7b261..e3d6f6b2e7 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -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 = () => { navigate('/settings/mcp/servers')} + onClick={() => navigate({ to: '/settings/mcp/servers' })} icon={} titleStyle={{ fontWeight: 500 }} /> @@ -93,14 +80,14 @@ const MCPSettings: FC = () => { navigate('/settings/mcp/builtin')} + onClick={() => navigate({ to: '/settings/mcp/builtin' })} icon={} titleStyle={{ fontWeight: 500 }} /> navigate('/settings/mcp/marketplaces')} + onClick={() => navigate({ to: '/settings/mcp/marketplaces' })} icon={} 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] || } titleStyle={{ fontWeight: 500 }} /> @@ -126,50 +113,7 @@ const MCPSettings: FC = () => { )} - - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - {providers.map((provider) => ( - } - /> - ))} - + @@ -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; diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index 6246804ed8..44f1355031 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -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 => { } const ProviderList: FC = () => { - const [searchParams, setSearchParams] = useSearchParams() + const search = useSearch({ strict: false }) as Record + 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() diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index ce9e208920..cfdc467bc7 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -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 { diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index f1b1186c43..21764a1a76 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -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 = () => { - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx index 166c2db77d..3a14c7aea5 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx @@ -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 diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx index 884c43e6b4..3fce99c79f 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx @@ -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) { diff --git a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx index a21de63764..fa4f7e9fc6 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx @@ -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 = () => { navigate('/settings/websearch/general')} + onClick={() => navigate({ to: '/settings/websearch/general' })} icon={} 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 ? ( {provider.name} @@ -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 ? ( {provider.name} @@ -151,11 +152,7 @@ const WebSearchSettings: FC = () => { )} - - } /> - } /> - } /> - + diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts new file mode 100644 index 0000000000..9afa09ec72 --- /dev/null +++ b/src/renderer/src/routeTree.gen.ts @@ -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() diff --git a/src/renderer/src/routes/README.md b/src/renderer/src/routes/README.md new file mode 100644 index 0000000000..91b82381d0 --- /dev/null +++ b/src/renderer/src/routes/README.md @@ -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
Knowledge Page
+} +``` + +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
Chat: {topicId}
+} +``` + +### 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 ( +
+ +
+
+ ) +} +``` + +## 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) => 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 `` 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. diff --git a/src/renderer/src/routes/README.zh-CN.md b/src/renderer/src/routes/README.zh-CN.md new file mode 100644 index 0000000000..138e4e8a9e --- /dev/null +++ b/src/renderer/src/routes/README.zh-CN.md @@ -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
Knowledge Page
+} +``` + +运行 `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
Chat: {topicId}
+} +``` + +### 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 ( +
+ +
+
+ ) +} +``` + +## 导航 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) => 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 `` 组件控制可见性 +- 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`、滚动位置等 diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx new file mode 100644 index 0000000000..07dcf9524c --- /dev/null +++ b/src/renderer/src/routes/__root.tsx @@ -0,0 +1,11 @@ +import NavigationHandler from '@renderer/handler/NavigationHandler' +import { createRootRoute, Outlet } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => ( + <> + + + + ) +}) diff --git a/src/renderer/src/routes/app.tsx b/src/renderer/src/routes/app.tsx new file mode 100644 index 0000000000..24d8dc2b3c --- /dev/null +++ b/src/renderer/src/routes/app.tsx @@ -0,0 +1,5 @@ +import { createFileRoute, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/app')({ + component: () => +}) diff --git a/src/renderer/src/routes/app/assistant.tsx b/src/renderer/src/routes/app/assistant.tsx new file mode 100644 index 0000000000..d6e9e458b5 --- /dev/null +++ b/src/renderer/src/routes/app/assistant.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/chat.tsx b/src/renderer/src/routes/app/chat.tsx new file mode 100644 index 0000000000..46f568edc1 --- /dev/null +++ b/src/renderer/src/routes/app/chat.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/code.tsx b/src/renderer/src/routes/app/code.tsx new file mode 100644 index 0000000000..6479af6682 --- /dev/null +++ b/src/renderer/src/routes/app/code.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/files.tsx b/src/renderer/src/routes/app/files.tsx new file mode 100644 index 0000000000..153bcd8596 --- /dev/null +++ b/src/renderer/src/routes/app/files.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/knowledge.tsx b/src/renderer/src/routes/app/knowledge.tsx new file mode 100644 index 0000000000..add14eb0e6 --- /dev/null +++ b/src/renderer/src/routes/app/knowledge.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/minapp/$appId.tsx b/src/renderer/src/routes/app/minapp/$appId.tsx new file mode 100644 index 0000000000..fbd89c6770 --- /dev/null +++ b/src/renderer/src/routes/app/minapp/$appId.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/minapp/index.tsx b/src/renderer/src/routes/app/minapp/index.tsx new file mode 100644 index 0000000000..41a47c45d3 --- /dev/null +++ b/src/renderer/src/routes/app/minapp/index.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/notes.tsx b/src/renderer/src/routes/app/notes.tsx new file mode 100644 index 0000000000..ab659b3809 --- /dev/null +++ b/src/renderer/src/routes/app/notes.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/paintings/$.tsx b/src/renderer/src/routes/app/paintings/$.tsx new file mode 100644 index 0000000000..d29fe06a8a --- /dev/null +++ b/src/renderer/src/routes/app/paintings/$.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/paintings/index.tsx b/src/renderer/src/routes/app/paintings/index.tsx new file mode 100644 index 0000000000..49374a570e --- /dev/null +++ b/src/renderer/src/routes/app/paintings/index.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/app/translate.tsx b/src/renderer/src/routes/app/translate.tsx new file mode 100644 index 0000000000..e85cb2ce8b --- /dev/null +++ b/src/renderer/src/routes/app/translate.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/home.tsx b/src/renderer/src/routes/home.tsx new file mode 100644 index 0000000000..5cf75bbeea --- /dev/null +++ b/src/renderer/src/routes/home.tsx @@ -0,0 +1,6 @@ +import LaunchpadPage from '@renderer/pages/launchpad/LaunchpadPage' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/home')({ + component: LaunchpadPage +}) diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx new file mode 100644 index 0000000000..4f341f0857 --- /dev/null +++ b/src/renderer/src/routes/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + beforeLoad: () => { + throw redirect({ to: '/home' }) + } +}) diff --git a/src/renderer/src/routes/settings.tsx b/src/renderer/src/routes/settings.tsx new file mode 100644 index 0000000000..15482a8a5d --- /dev/null +++ b/src/renderer/src/routes/settings.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/about.tsx b/src/renderer/src/routes/settings/about.tsx new file mode 100644 index 0000000000..6c26b0091b --- /dev/null +++ b/src/renderer/src/routes/settings/about.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/api-server.tsx b/src/renderer/src/routes/settings/api-server.tsx new file mode 100644 index 0000000000..3715efa898 --- /dev/null +++ b/src/renderer/src/routes/settings/api-server.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/data.tsx b/src/renderer/src/routes/settings/data.tsx new file mode 100644 index 0000000000..a4f7d20246 --- /dev/null +++ b/src/renderer/src/routes/settings/data.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/display.tsx b/src/renderer/src/routes/settings/display.tsx new file mode 100644 index 0000000000..ef53cb8fec --- /dev/null +++ b/src/renderer/src/routes/settings/display.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/docprocess.tsx b/src/renderer/src/routes/settings/docprocess.tsx new file mode 100644 index 0000000000..9ff2cef785 --- /dev/null +++ b/src/renderer/src/routes/settings/docprocess.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/general.tsx b/src/renderer/src/routes/settings/general.tsx new file mode 100644 index 0000000000..788de1139f --- /dev/null +++ b/src/renderer/src/routes/settings/general.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/index.tsx b/src/renderer/src/routes/settings/index.tsx new file mode 100644 index 0000000000..96a3545dc5 --- /dev/null +++ b/src/renderer/src/routes/settings/index.tsx @@ -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' }) + } +}) diff --git a/src/renderer/src/routes/settings/mcp.tsx b/src/renderer/src/routes/settings/mcp.tsx new file mode 100644 index 0000000000..3adb2c94d9 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/mcp/$.tsx b/src/renderer/src/routes/settings/mcp/$.tsx new file mode 100644 index 0000000000..98d221899a --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/$.tsx @@ -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
Provider not found
+ } + + return +} + +export const Route = createFileRoute('/settings/mcp/$')({ + component: ProviderPage +}) diff --git a/src/renderer/src/routes/settings/mcp/builtin.tsx b/src/renderer/src/routes/settings/mcp/builtin.tsx new file mode 100644 index 0000000000..aa1cbdf3ac --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/builtin.tsx @@ -0,0 +1,12 @@ +import BuiltinMCPServerList from '@renderer/pages/settings/MCPSettings/BuiltinMCPServerList' +import { createFileRoute } from '@tanstack/react-router' + +const BuiltinWrapper = () => ( +
+ +
+) + +export const Route = createFileRoute('/settings/mcp/builtin')({ + component: BuiltinWrapper +}) diff --git a/src/renderer/src/routes/settings/mcp/index.tsx b/src/renderer/src/routes/settings/mcp/index.tsx new file mode 100644 index 0000000000..6a297dd0b7 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/index.tsx @@ -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' }) + } +}) diff --git a/src/renderer/src/routes/settings/mcp/marketplaces.tsx b/src/renderer/src/routes/settings/mcp/marketplaces.tsx new file mode 100644 index 0000000000..2908f0b961 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/marketplaces.tsx @@ -0,0 +1,12 @@ +import McpMarketList from '@renderer/pages/settings/MCPSettings/McpMarketList' +import { createFileRoute } from '@tanstack/react-router' + +const MarketplacesWrapper = () => ( +
+ +
+) + +export const Route = createFileRoute('/settings/mcp/marketplaces')({ + component: MarketplacesWrapper +}) diff --git a/src/renderer/src/routes/settings/mcp/mcp-install.tsx b/src/renderer/src/routes/settings/mcp/mcp-install.tsx new file mode 100644 index 0000000000..d91dd5389a --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/mcp-install.tsx @@ -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 = () => ( + + + +) + +export const Route = createFileRoute('/settings/mcp/mcp-install')({ + component: McpInstallWrapper +}) diff --git a/src/renderer/src/routes/settings/mcp/npx-search.tsx b/src/renderer/src/routes/settings/mcp/npx-search.tsx new file mode 100644 index 0000000000..a6d3754042 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/npx-search.tsx @@ -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 ( + + + + ) +} + +export const Route = createFileRoute('/settings/mcp/npx-search')({ + component: NpxSearchWrapper +}) diff --git a/src/renderer/src/routes/settings/mcp/servers.tsx b/src/renderer/src/routes/settings/mcp/servers.tsx new file mode 100644 index 0000000000..73587cbba9 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/servers.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/mcp/settings.$serverId.tsx b/src/renderer/src/routes/settings/mcp/settings.$serverId.tsx new file mode 100644 index 0000000000..ad4e2b62e2 --- /dev/null +++ b/src/renderer/src/routes/settings/mcp/settings.$serverId.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/memory.tsx b/src/renderer/src/routes/settings/memory.tsx new file mode 100644 index 0000000000..712f212aba --- /dev/null +++ b/src/renderer/src/routes/settings/memory.tsx @@ -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 +}) diff --git a/src/renderer/src/routes/settings/model.tsx b/src/renderer/src/routes/settings/model.tsx new file mode 100644 index 0000000000..ca5f86873a --- /dev/null +++ b/src/renderer/src/routes/settings/model.tsx @@ -0,0 +1,6 @@ +import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/model')({ + component: ModelSettings +}) diff --git a/src/renderer/src/routes/settings/notes.tsx b/src/renderer/src/routes/settings/notes.tsx new file mode 100644 index 0000000000..bff74d3d99 --- /dev/null +++ b/src/renderer/src/routes/settings/notes.tsx @@ -0,0 +1,6 @@ +import NotesSettings from '@renderer/pages/notes/NotesSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/notes')({ + component: NotesSettings +}) diff --git a/src/renderer/src/routes/settings/provider.tsx b/src/renderer/src/routes/settings/provider.tsx new file mode 100644 index 0000000000..f99495ea5d --- /dev/null +++ b/src/renderer/src/routes/settings/provider.tsx @@ -0,0 +1,6 @@ +import { ProviderList } from '@renderer/pages/settings/ProviderSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/provider')({ + component: ProviderList +}) diff --git a/src/renderer/src/routes/settings/quickAssistant.tsx b/src/renderer/src/routes/settings/quickAssistant.tsx new file mode 100644 index 0000000000..3f737c1469 --- /dev/null +++ b/src/renderer/src/routes/settings/quickAssistant.tsx @@ -0,0 +1,6 @@ +import QuickAssistantSettings from '@renderer/pages/settings/QuickAssistantSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/quickAssistant')({ + component: QuickAssistantSettings +}) diff --git a/src/renderer/src/routes/settings/quickphrase.tsx b/src/renderer/src/routes/settings/quickphrase.tsx new file mode 100644 index 0000000000..3c2d2478dc --- /dev/null +++ b/src/renderer/src/routes/settings/quickphrase.tsx @@ -0,0 +1,6 @@ +import QuickPhraseSettings from '@renderer/pages/settings/QuickPhraseSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/quickphrase')({ + component: QuickPhraseSettings +}) diff --git a/src/renderer/src/routes/settings/selectionAssistant.tsx b/src/renderer/src/routes/settings/selectionAssistant.tsx new file mode 100644 index 0000000000..297a25118c --- /dev/null +++ b/src/renderer/src/routes/settings/selectionAssistant.tsx @@ -0,0 +1,6 @@ +import SelectionAssistantSettings from '@renderer/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/selectionAssistant')({ + component: SelectionAssistantSettings +}) diff --git a/src/renderer/src/routes/settings/shortcut.tsx b/src/renderer/src/routes/settings/shortcut.tsx new file mode 100644 index 0000000000..462d3f7a70 --- /dev/null +++ b/src/renderer/src/routes/settings/shortcut.tsx @@ -0,0 +1,6 @@ +import ShortcutSettings from '@renderer/pages/settings/ShortcutSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/shortcut')({ + component: ShortcutSettings +}) diff --git a/src/renderer/src/routes/settings/websearch.tsx b/src/renderer/src/routes/settings/websearch.tsx new file mode 100644 index 0000000000..3c9279bb9d --- /dev/null +++ b/src/renderer/src/routes/settings/websearch.tsx @@ -0,0 +1,6 @@ +import WebSearchSettings from '@renderer/pages/settings/WebSearchSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/websearch')({ + component: WebSearchSettings +}) diff --git a/src/renderer/src/routes/settings/websearch/general.tsx b/src/renderer/src/routes/settings/websearch/general.tsx new file mode 100644 index 0000000000..531e916ee6 --- /dev/null +++ b/src/renderer/src/routes/settings/websearch/general.tsx @@ -0,0 +1,6 @@ +import WebSearchGeneralSettings from '@renderer/pages/settings/WebSearchSettings/WebSearchGeneralSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/websearch/general')({ + component: WebSearchGeneralSettings +}) diff --git a/src/renderer/src/routes/settings/websearch/index.tsx b/src/renderer/src/routes/settings/websearch/index.tsx new file mode 100644 index 0000000000..d722636bba --- /dev/null +++ b/src/renderer/src/routes/settings/websearch/index.tsx @@ -0,0 +1,8 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +// /settings/websearch/ 重定向到 /settings/websearch/general +export const Route = createFileRoute('/settings/websearch/')({ + beforeLoad: () => { + throw redirect({ to: '/settings/websearch/general' }) + } +}) diff --git a/src/renderer/src/routes/settings/websearch/provider.$providerId.tsx b/src/renderer/src/routes/settings/websearch/provider.$providerId.tsx new file mode 100644 index 0000000000..dbfab96c98 --- /dev/null +++ b/src/renderer/src/routes/settings/websearch/provider.$providerId.tsx @@ -0,0 +1,6 @@ +import WebSearchProviderSettings from '@renderer/pages/settings/WebSearchSettings/WebSearchProviderSettings' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/settings/websearch/provider/$providerId')({ + component: WebSearchProviderSettings +}) diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 58fa6fbea1..85be71c515 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -23,9 +23,9 @@ import { } from '@renderer/utils/messageUtils/create' import { filterContextMessages } from '@renderer/utils/messageUtils/filters' import { getMainTextContent } from '@renderer/utils/messageUtils/find' +import type { UseNavigateResult } from '@tanstack/react-router' import dayjs from 'dayjs' import { t } from 'i18next' -import type { NavigateFunction } from 'react-router' import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService' import { EVENT_NAMES, EventEmitter } from './EventService' @@ -68,14 +68,14 @@ export function deleteMessageFiles(message: Message) { }) } -export async function locateToMessage(navigate: NavigateFunction, message: Message) { +export async function locateToMessage(navigate: UseNavigateResult, message: Message) { await modelGenerating() SearchPopup.hide() const assistant = getAssistantById(message.assistantId) const topic = await getTopicById(message.topicId) - navigate('/', { state: { assistant, topic } }) + navigate({ to: '/app/chat', search: { assistantId: assistant?.id, topicId: topic?.id } }) setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) setTimeout(() => EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id), 300) diff --git a/src/renderer/src/services/NavigationService.ts b/src/renderer/src/services/NavigationService.ts index 3c4ae54abe..ac4747b160 100644 --- a/src/renderer/src/services/NavigationService.ts +++ b/src/renderer/src/services/NavigationService.ts @@ -1,14 +1,15 @@ -import type { NavigateFunction } from 'react-router-dom' +import type { UseNavigateResult } from '@tanstack/react-router' +// Tab 导航服务 - 用于在非 React 组件中进行路由导航 interface INavigationService { - navigate: NavigateFunction | null - setNavigate: (navigateFunc: NavigateFunction) => void + navigate: UseNavigateResult | null + setNavigate: (navigateFunc: UseNavigateResult) => void } const NavigationService: INavigationService = { navigate: null, - setNavigate: (navigateFunc: NavigateFunction): void => { + setNavigate: (navigateFunc: UseNavigateResult): void => { NavigationService.navigate = navigateFunc window.navigate = NavigationService.navigate } diff --git a/src/renderer/src/services/TabLRUManager.ts b/src/renderer/src/services/TabLRUManager.ts new file mode 100644 index 0000000000..c536bc5d76 --- /dev/null +++ b/src/renderer/src/services/TabLRUManager.ts @@ -0,0 +1,198 @@ +import { loggerService } from '@logger' +import type { Tab } from '@shared/data/cache/cacheValueTypes' + +const logger = loggerService.withContext('TabLRU') + +/** + * Tab LRU limits configuration + * + * Controls when inactive tabs should be hibernated to save memory. + * TODO: 后续可从偏好设置注入 + */ +export const TAB_LIMITS = { + /** + * 软上限:活跃标签数超过此值时触发 LRU 休眠 + * 默认 10,可根据实际内存使用情况调整 + */ + softCap: 10, + + /** + * 硬保险丝:极端兜底,防止 runaway + * 当活跃标签数超过此值时,强制休眠超额部分 + */ + hardCap: 22 +} + +export type TabLimits = typeof TAB_LIMITS + +/** + * TabLRUManager - 管理标签页的 LRU 休眠策略 + * + * 功能: + * - 当活跃标签数超过软上限时,选择 LRU 候选进行休眠 + * - 硬保险丝作为极端兜底,防止内存失控 + * - 支持豁免机制:当前标签、首页、置顶标签不参与休眠 + */ +export class TabLRUManager { + private softCap: number + private hardCap: number + + constructor(limits: TabLimits = TAB_LIMITS) { + this.softCap = limits.softCap + this.hardCap = limits.hardCap + } + + /** + * 检查并返回需要休眠的标签 ID 列表 + * + * 策略: + * - 超过 softCap:休眠到 softCap + * - 超过 hardCap:强制休眠到 softCap(忽略部分豁免,仅保留当前+首页) + * + * @param tabs 所有标签 + * @param activeTabId 当前活动标签 ID + * @returns 需要休眠的标签 ID 数组 + */ + checkAndGetDormantCandidates(tabs: Tab[], activeTabId: string): string[] { + const activeTabs = tabs.filter((t) => !t.isDormant) + const activeCount = activeTabs.length + + // 未超软上限,无需休眠 + if (activeCount <= this.softCap) { + return [] + } + + const isHardCapTriggered = activeCount > this.hardCap + + // 获取候选列表 + // 硬保险丝触发时,使用更宽松的豁免规则(仅保留当前+首页) + const candidates = isHardCapTriggered + ? this.getHardCapCandidates(activeTabs, activeTabId) + : this.getLRUCandidates(activeTabs, activeTabId) + + // 计算需要休眠的数量:始终休眠到 softCap + let toHibernateCount = activeCount - this.softCap + + if (isHardCapTriggered) { + logger.warn('Hard cap triggered - using relaxed exemption rules', { + activeCount, + hardCap: this.hardCap, + softCap: this.softCap, + toHibernate: toHibernateCount + }) + } + + // 只能休眠可用的候选数量 + toHibernateCount = Math.min(toHibernateCount, candidates.length) + + // 检查是否能达到目标 + const afterHibernation = activeCount - toHibernateCount + if (isHardCapTriggered && afterHibernation > this.hardCap) { + // 极端情况:即使放宽豁免,仍无法降到 hardCap 以下 + logger.error('Cannot guarantee hard cap - insufficient candidates', { + activeCount, + candidatesAvailable: candidates.length, + willHibernate: toHibernateCount, + afterHibernation, + hardCap: this.hardCap + }) + } else if (afterHibernation > this.softCap) { + // 一般情况:无法降到 softCap,但仍在 hardCap 以下 + logger.warn('Cannot reach soft cap - limited by available candidates', { + activeCount, + candidatesAvailable: candidates.length, + willHibernate: toHibernateCount, + afterHibernation, + softCap: this.softCap + }) + } + + const result = candidates.slice(0, toHibernateCount).map((t) => t.id) + + if (result.length > 0) { + logger.info('Tabs selected for hibernation', { + count: result.length, + ids: result, + activeCount, + softCap: this.softCap, + hardCapTriggered: isHardCapTriggered + }) + } + + return result + } + + /** + * 硬保险丝候选列表(仅豁免当前标签和首页) + */ + private getHardCapCandidates(tabs: Tab[], activeTabId: string): Tab[] { + return tabs + .filter((tab) => !this.isHardExempt(tab, activeTabId)) + .sort((a, b) => (a.lastAccessTime ?? 0) - (b.lastAccessTime ?? 0)) + } + + /** + * 硬保险丝豁免判断(更严格,仅保留当前+首页) + */ + private isHardExempt(tab: Tab, activeTabId: string): boolean { + return ( + tab.id === activeTabId || // 当前活动标签 + tab.id === 'home' || // 首页 + tab.isDormant === true // 已休眠的不再参与 + ) + // 注意:isPinned 在硬保险丝触发时不再豁免 + } + + /** + * 获取 LRU 候选列表(排除豁免项,按访问时间升序) + */ + private getLRUCandidates(tabs: Tab[], activeTabId: string): Tab[] { + return tabs + .filter((tab) => !this.isExempt(tab, activeTabId)) + .sort((a, b) => (a.lastAccessTime ?? 0) - (b.lastAccessTime ?? 0)) + } + + /** + * 判断标签是否豁免休眠 + * + * 豁免条件: + * - 当前活动标签 + * - 首页标签 (id === 'home') + * - 置顶标签 (isPinned) + * - 已休眠的标签(不重复处理) + */ + private isExempt(tab: Tab, activeTabId: string): boolean { + return ( + tab.id === activeTabId || // 当前活动标签 + tab.id === 'home' || // 首页 + tab.isPinned === true || // 置顶标签 + tab.isDormant === true // 已休眠的不再参与 + ) + } + + /** + * 更新软上限(供未来设置页使用) + */ + updateSoftCap(newSoftCap: number): void { + this.softCap = newSoftCap + logger.info('SoftCap updated', { newSoftCap }) + } + + /** + * 更新硬上限(供未来设置页使用) + */ + updateHardCap(newHardCap: number): void { + this.hardCap = newHardCap + logger.info('HardCap updated', { newHardCap }) + } + + /** + * 获取当前配置 + */ + getLimits(): TabLimits { + return { + softCap: this.softCap, + hardCap: this.hardCap + } + } +} diff --git a/src/renderer/src/services/TabsService.ts b/src/renderer/src/services/TabsService.ts index a0c22c488b..e94faea192 100644 --- a/src/renderer/src/services/TabsService.ts +++ b/src/renderer/src/services/TabsService.ts @@ -55,12 +55,12 @@ class TabsService { // 使用 NavigationService 导航到新的标签页 if (NavigationService.navigate) { - NavigationService.navigate(lastTab.path) + NavigationService.navigate({ to: lastTab.path }) } else { logger.warn('Navigation service not ready, will navigate on next render') setTimeout(() => { if (NavigationService.navigate) { - NavigationService.navigate(lastTab.path) + NavigationService.navigate({ to: lastTab.path }) } }, 100) } @@ -133,7 +133,7 @@ class TabsService { // 导航到对应页面 if (NavigationService.navigate) { - NavigationService.navigate(tab.path) + NavigationService.navigate({ to: tab.path }) } return true diff --git a/src/renderer/src/services/__tests__/TabLRUManager.test.ts b/src/renderer/src/services/__tests__/TabLRUManager.test.ts new file mode 100644 index 0000000000..076f680a99 --- /dev/null +++ b/src/renderer/src/services/__tests__/TabLRUManager.test.ts @@ -0,0 +1,260 @@ +import type { Tab } from '@shared/data/cache/cacheValueTypes' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { TAB_LIMITS, TabLRUManager } from '../TabLRUManager' + +// Helper to create a mock tab +const createTab = (id: string, overrides: Partial = {}): Tab => ({ + id, + type: 'route', + url: `/${id}`, + title: id, + lastAccessTime: Date.now(), + isDormant: false, + isPinned: false, + ...overrides +}) + +describe('TabLRUManager', () => { + let manager: TabLRUManager + + beforeEach(() => { + manager = new TabLRUManager() + // Suppress logger output during tests + vi.spyOn(console, 'info').mockImplementation(() => {}) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + describe('constructor', () => { + it('should use default limits', () => { + const limits = manager.getLimits() + expect(limits.softCap).toBe(TAB_LIMITS.softCap) + expect(limits.hardCap).toBe(TAB_LIMITS.hardCap) + }) + + it('should accept custom limits', () => { + const customManager = new TabLRUManager({ softCap: 5, hardCap: 15 }) + const limits = customManager.getLimits() + expect(limits.softCap).toBe(5) + expect(limits.hardCap).toBe(15) + }) + }) + + describe('checkAndGetDormantCandidates', () => { + describe('when under soft cap', () => { + it('should return empty array when active tabs <= softCap', () => { + const tabs = Array.from({ length: TAB_LIMITS.softCap }, (_, i) => createTab(`tab-${i}`)) + const result = manager.checkAndGetDormantCandidates(tabs, 'tab-0') + expect(result).toEqual([]) + }) + + it('should return empty array for 1 tab', () => { + const tabs = [createTab('tab-0')] + const result = manager.checkAndGetDormantCandidates(tabs, 'tab-0') + expect(result).toEqual([]) + }) + }) + + describe('when exceeding soft cap', () => { + it('should return oldest tabs when exceeding softCap', () => { + const now = Date.now() + const tabs = Array.from({ length: TAB_LIMITS.softCap + 3 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.softCap + 2}`) + + // Should hibernate 3 tabs (to get back to softCap) + expect(result.length).toBe(3) + // Should be the oldest tabs (lowest access times) + expect(result).toContain('tab-0') + expect(result).toContain('tab-1') + expect(result).toContain('tab-2') + }) + + it('should not hibernate the active tab', () => { + const now = Date.now() + const tabs = Array.from({ length: TAB_LIMITS.softCap + 2 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + + // Make tab-0 the oldest but also the active tab + const result = manager.checkAndGetDormantCandidates(tabs, 'tab-0') + + expect(result).not.toContain('tab-0') + }) + + it('should not hibernate the home tab', () => { + const now = Date.now() + const tabs = [ + createTab('home', { lastAccessTime: now - 10000 }), // Oldest + ...Array.from({ length: TAB_LIMITS.softCap + 1 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.softCap}`) + + expect(result).not.toContain('home') + }) + + it('should not hibernate pinned tabs', () => { + const now = Date.now() + const tabs = [ + createTab('pinned-tab', { lastAccessTime: now - 10000, isPinned: true }), // Oldest but pinned + ...Array.from({ length: TAB_LIMITS.softCap + 1 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.softCap}`) + + expect(result).not.toContain('pinned-tab') + }) + + it('should not hibernate already dormant tabs', () => { + const now = Date.now() + const tabs = [ + createTab('dormant-tab', { lastAccessTime: now - 10000, isDormant: true }), + ...Array.from({ length: TAB_LIMITS.softCap + 1 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.softCap}`) + + expect(result).not.toContain('dormant-tab') + }) + }) + + describe('hard cap behavior', () => { + it('should use relaxed exemption rules when exceeding hard cap', () => { + const now = Date.now() + // Create tabs exceeding hard cap, with one pinned oldest tab + const tabs = [ + createTab('pinned-old', { lastAccessTime: now - 20000, isPinned: true }), + ...Array.from({ length: TAB_LIMITS.hardCap + 2 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.hardCap + 1}`) + + // Hard cap triggered: pinned tabs are no longer exempt (except home and active) + expect(result).toContain('pinned-old') + }) + + it('should still protect home and active tabs in hard cap mode', () => { + const now = Date.now() + const tabs = [ + createTab('home', { lastAccessTime: now - 30000 }), + ...Array.from({ length: TAB_LIMITS.hardCap + 2 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const activeTabId = `tab-${TAB_LIMITS.hardCap + 1}` + const result = manager.checkAndGetDormantCandidates(tabs, activeTabId) + + expect(result).not.toContain('home') + expect(result).not.toContain(activeTabId) + }) + }) + + describe('edge cases', () => { + it('should handle empty tabs array', () => { + const result = manager.checkAndGetDormantCandidates([], 'any-id') + expect(result).toEqual([]) + }) + + it('should handle tabs with undefined lastAccessTime', () => { + const tabs = Array.from({ length: TAB_LIMITS.softCap + 2 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: undefined }) + ) + + // Should not throw + const result = manager.checkAndGetDormantCandidates(tabs, `tab-${TAB_LIMITS.softCap + 1}`) + expect(Array.isArray(result)).toBe(true) + }) + + it('should handle when all tabs are exempt', () => { + const now = Date.now() + // All tabs are pinned + const tabs = Array.from({ length: TAB_LIMITS.softCap + 3 }, (_, i) => + createTab(`tab-${i}`, { lastAccessTime: now + i * 1000, isPinned: true }) + ) + + const result = manager.checkAndGetDormantCandidates(tabs, 'tab-0') + + // Should return empty (no candidates available) + expect(result.length).toBeLessThan(3) + }) + + it('should handle mixed dormant and active tabs correctly', () => { + const now = Date.now() + const tabs = [ + // 5 dormant tabs (should not count toward active) + ...Array.from({ length: 5 }, (_, i) => + createTab(`dormant-${i}`, { isDormant: true, lastAccessTime: now - i * 1000 }) + ), + // Active tabs exceeding soft cap + ...Array.from({ length: TAB_LIMITS.softCap + 2 }, (_, i) => + createTab(`active-${i}`, { lastAccessTime: now + i * 1000 }) + ) + ] + + const result = manager.checkAndGetDormantCandidates(tabs, `active-${TAB_LIMITS.softCap + 1}`) + + // Should only consider active tabs + expect(result.every((id) => id.startsWith('active-'))).toBe(true) + expect(result.length).toBe(2) // Need to hibernate 2 to reach soft cap + }) + }) + }) + + describe('updateSoftCap', () => { + it('should update soft cap value', () => { + manager.updateSoftCap(15) + expect(manager.getLimits().softCap).toBe(15) + }) + }) + + describe('updateHardCap', () => { + it('should update hard cap value', () => { + manager.updateHardCap(30) + expect(manager.getLimits().hardCap).toBe(30) + }) + }) + + describe('getLimits', () => { + it('should return current limits', () => { + const customManager = new TabLRUManager({ softCap: 8, hardCap: 20 }) + const limits = customManager.getLimits() + + expect(limits).toEqual({ softCap: 8, hardCap: 20 }) + }) + }) + + describe('LRU ordering', () => { + it('should correctly order tabs by lastAccessTime', () => { + const customManager = new TabLRUManager({ softCap: 3, hardCap: 10 }) + const now = Date.now() + + const tabs = [ + createTab('tab-oldest', { lastAccessTime: now - 3000 }), + createTab('tab-newest', { lastAccessTime: now }), + createTab('tab-middle', { lastAccessTime: now - 1000 }), + createTab('tab-second-oldest', { lastAccessTime: now - 2000 }), + createTab('tab-active', { lastAccessTime: now + 1000 }) // Active tab + ] + + const result = customManager.checkAndGetDormantCandidates(tabs, 'tab-active') + + // Should hibernate the 2 oldest tabs + expect(result.length).toBe(2) + expect(result[0]).toBe('tab-oldest') + expect(result[1]).toBe('tab-second-oldest') + }) + }) +}) diff --git a/src/renderer/src/utils/__tests__/routeTitle.test.ts b/src/renderer/src/utils/__tests__/routeTitle.test.ts new file mode 100644 index 0000000000..688af39434 --- /dev/null +++ b/src/renderer/src/utils/__tests__/routeTitle.test.ts @@ -0,0 +1,133 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock i18n before importing the module +vi.mock('@renderer/i18n', () => ({ + default: { + t: vi.fn((key: string) => { + const translations: Record = { + 'title.home': '首页', + 'common.chat': '聊天', + 'title.store': '助手库', + 'title.paintings': '绘画', + 'title.translate': '翻译', + 'title.apps': '小程序', + 'title.knowledge': '知识库', + 'title.files': '文件', + 'title.code': 'Code', + 'title.notes': '笔记', + 'title.settings': '设置' + } + return translations[key] || key + }) + } +})) + +import { getDefaultRouteTitle, getRouteTitleKey } from '../routeTitle' + +describe('routeTitle', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getDefaultRouteTitle', () => { + describe('exact route matches', () => { + it.each([ + ['/', '首页'], + ['/home', '首页'], + ['/app/chat', '聊天'], + ['/app/assistant', '助手库'], + ['/app/paintings', '绘画'], + ['/app/translate', '翻译'], + ['/app/minapp', '小程序'], + ['/app/knowledge', '知识库'], + ['/app/files', '文件'], + ['/app/code', 'Code'], + ['/app/notes', '笔记'], + ['/settings', '设置'] + ])('should return correct title for %s', (url, expectedTitle) => { + expect(getDefaultRouteTitle(url)).toBe(expectedTitle) + }) + }) + + describe('nested route matches', () => { + it('should match base path for nested routes', () => { + expect(getDefaultRouteTitle('/app/chat/topic-123')).toBe('聊天') + expect(getDefaultRouteTitle('/settings/provider')).toBe('设置') + expect(getDefaultRouteTitle('/settings/mcp/servers')).toBe('设置') + expect(getDefaultRouteTitle('/app/paintings/zhipu')).toBe('绘画') + }) + }) + + describe('URL with query params and hash', () => { + it('should handle URLs with query parameters', () => { + expect(getDefaultRouteTitle('/app/chat?topicId=123')).toBe('聊天') + expect(getDefaultRouteTitle('/settings/provider?id=openai')).toBe('设置') + }) + + it('should handle URLs with hash', () => { + expect(getDefaultRouteTitle('/app/knowledge#section1')).toBe('知识库') + }) + + it('should handle URLs with both query and hash', () => { + expect(getDefaultRouteTitle('/app/chat?id=1#message-5')).toBe('聊天') + }) + }) + + describe('unknown routes', () => { + it('should return last segment for unknown routes', () => { + expect(getDefaultRouteTitle('/unknown')).toBe('unknown') + expect(getDefaultRouteTitle('/foo/bar/baz')).toBe('baz') + }) + + it('should return pathname for root-like unknown routes', () => { + expect(getDefaultRouteTitle('/x')).toBe('x') + }) + }) + + describe('edge cases', () => { + it('should handle trailing slashes', () => { + expect(getDefaultRouteTitle('/app/chat/')).toBe('聊天') + expect(getDefaultRouteTitle('/settings/')).toBe('设置') + }) + + it('should handle double slashes (protocol-relative URL)', () => { + // '//chat' is a protocol-relative URL, so 'chat' becomes the hostname + // This is expected behavior per URL standard + expect(getDefaultRouteTitle('//chat')).toBe('首页') + }) + + it('should handle relative-like paths', () => { + // URL constructor with base will normalize these + expect(getDefaultRouteTitle('app/chat')).toBe('聊天') + expect(getDefaultRouteTitle('./app/chat')).toBe('聊天') + }) + }) + }) + + describe('getRouteTitleKey', () => { + describe('exact matches', () => { + it.each([ + ['/', 'title.home'], + ['/app/chat', 'common.chat'], + ['/app/assistant', 'title.store'], + ['/settings', 'title.settings'] + ])('should return i18n key for %s', (url, expectedKey) => { + expect(getRouteTitleKey(url)).toBe(expectedKey) + }) + }) + + describe('base path matches', () => { + it('should return base path key for nested routes', () => { + expect(getRouteTitleKey('/app/chat/topic-123')).toBe('common.chat') + expect(getRouteTitleKey('/settings/provider')).toBe('title.settings') + }) + }) + + describe('unknown routes', () => { + it('should return undefined for unknown routes', () => { + expect(getRouteTitleKey('/unknown')).toBeUndefined() + expect(getRouteTitleKey('/foo/bar')).toBeUndefined() + }) + }) + }) +}) diff --git a/src/renderer/src/utils/routeTitle.ts b/src/renderer/src/utils/routeTitle.ts new file mode 100644 index 0000000000..df9588d686 --- /dev/null +++ b/src/renderer/src/utils/routeTitle.ts @@ -0,0 +1,78 @@ +import i18n from '@renderer/i18n' + +/** Base URL for parsing relative route paths */ +const BASE_URL = 'https://www.cherry-ai.com/' + +/** + * Route to i18n key mapping for default tab titles + */ +const routeTitleKeys: Record = { + '/': 'title.home', + '/home': 'title.home', + '/app/chat': 'common.chat', + '/app/assistant': 'title.store', + '/app/paintings': 'title.paintings', + '/app/translate': 'title.translate', + '/app/minapp': 'title.apps', + '/app/knowledge': 'title.knowledge', + '/app/files': 'title.files', + '/app/code': 'title.code', + '/app/notes': 'title.notes', + '/settings': 'title.settings' +} + +/** + * Get the base path for route matching + * For /app/* routes, returns first two segments (e.g., '/app/chat') + * For other routes, returns first segment (e.g., '/settings') + */ +function getBasePath(pathname: string): string { + const segments = pathname.split('/').filter(Boolean) + if (segments[0] === 'app' && segments.length >= 2) { + return '/' + segments.slice(0, 2).join('/') + } + return '/' + (segments[0] || '') +} + +/** + * Get the default title for a route URL + * + * @param url - Route URL (e.g., '/settings', '/app/chat/123') + * @returns Translated title or URL path fallback + * + * @example + * getDefaultRouteTitle('/settings') // '设置' + * getDefaultRouteTitle('/app/chat/abc123') // '助手' + * getDefaultRouteTitle('/unknown') // 'unknown' + */ +export function getDefaultRouteTitle(url: string): string { + const sanitizedUrl = new URL(url, BASE_URL).pathname + + // Try exact match first + const exactKey = routeTitleKeys[sanitizedUrl] + if (exactKey) { + return i18n.t(exactKey) + } + + // Try matching base path + const baseKey = routeTitleKeys[getBasePath(sanitizedUrl)] + if (baseKey) { + return i18n.t(baseKey) + } + + // Fallback to last segment of pathname + const segments = sanitizedUrl.split('/').filter(Boolean) + return segments.pop() || sanitizedUrl +} + +/** + * Get the i18n key for a route (without translating) + */ +export function getRouteTitleKey(url: string): string | undefined { + const sanitizedUrl = new URL(url, BASE_URL).pathname + + const exactKey = routeTitleKeys[sanitizedUrl] + if (exactKey) return exactKey + + return routeTitleKeys[getBasePath(sanitizedUrl)] +}