feat: implement app state management and routing system

- Added AppStateService for managing application state in the database, including methods for getting, setting, and deleting state.
- Introduced new API endpoint for app state storage with GET and PUT methods.
- Integrated TanStack Router for routing, enabling a tabbed interface with dynamic routing and state synchronization.
- Created AppShell component to manage layout and routing, including a sidebar and tab management.
- Developed useTabs hook for handling tab state, including adding, closing, and switching tabs.
- Updated package.json and yarn.lock to include necessary dependencies for routing and state management.

This commit enhances the application's architecture by providing a robust state management system and a flexible routing mechanism, improving user experience and maintainability.
This commit is contained in:
MyPrototypeWhat 2025-11-25 19:06:42 +08:00
parent cf7b4dd07b
commit aae39e3365
16 changed files with 1418 additions and 56 deletions

View File

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

View File

@ -182,7 +182,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",
@ -220,8 +222,8 @@
"@types/mime-types": "^3",
"@types/node": "^22.17.1",
"@types/pako": "^1.0.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-transition-group": "^4.4.12",
"@types/react-window": "^1",

View File

@ -18,6 +18,25 @@ export type { ConcreteApiPaths } from './apiPaths'
* enabling full TypeScript type checking across IPC boundaries.
*/
export interface ApiSchemas {
/**
* App State storage endpoint
* @example GET /app/state/tabs_state
* @example PUT /app/state/tabs_state { "value": {...} }
*/
'/app/state/:key': {
/** Get app state by key */
GET: {
params: { key: string }
response: any
}
/** Save app state */
PUT: {
params: { key: string }
body: any
response: { success: boolean }
}
}
/**
* Test items collection endpoint
* @example GET /test/items?page=1&limit=10&search=hello

View File

@ -26,6 +26,9 @@ export type UseCacheSchema = {
'topic.active': CacheValueTypes.CacheTopic | null
'topic.renaming': string[]
'topic.newly_renamed': string[]
// UI State
'ui.activeTabId': string
}
export const DefaultUseCache: UseCacheSchema = {
@ -56,7 +59,10 @@ export const DefaultUseCache: UseCacheSchema = {
// Topic management
'topic.active': null,
'topic.renaming': [],
'topic.newly_renamed': []
'topic.newly_renamed': [],
// UI State
'ui.activeTabId': ''
}
/**

View File

@ -5,6 +5,7 @@
* TypeScript will error if any endpoint is missing.
*/
import { appStateService } from '@data/services/AppStateService'
import { TestService } from '@data/services/TestService'
import type { ApiImplementation } from '@shared/data/api/apiSchemas'
@ -16,6 +17,18 @@ const testService = TestService.getInstance()
* Must implement every path+method combination from ApiSchemas
*/
export const apiHandlers: ApiImplementation = {
'/app/state/:key': {
GET: async ({ params }) => {
const state = await appStateService.getState(params.key)
return state
},
PUT: async ({ params, body }) => {
await appStateService.setState(params.key, body)
return { success: true }
}
},
'/test/items': {
GET: async ({ query }) => {
return await testService.getItems({

View File

@ -0,0 +1,100 @@
import { dbService } from '@data/db/DbService'
import { appStateTable } from '@data/db/schemas/appState'
import { loggerService } from '@logger'
import { eq } from 'drizzle-orm'
const logger = loggerService.withContext('AppStateService')
/**
* Service for managing application state in the database.
* Provides key-value storage for persisting UI state like tabs, window positions, etc.
*/
export class AppStateService {
private static instance: AppStateService
private constructor() {}
public static getInstance(): AppStateService {
if (!AppStateService.instance) {
AppStateService.instance = new AppStateService()
}
return AppStateService.instance
}
/**
* Get app state by key
* @param key - The state key to retrieve
* @returns The stored value or null if not found
*/
async getState<T = unknown>(key: string): Promise<T | null> {
try {
const db = dbService.getDb()
const result = await db.select().from(appStateTable).where(eq(appStateTable.key, key)).limit(1)
if (result.length === 0) {
logger.debug('App state not found', { key })
return null
}
logger.debug('Retrieved app state', { key })
return result[0].value as T
} catch (error) {
logger.error('Failed to get app state', error as Error, { key })
throw error
}
}
/**
* Save app state by key (upsert)
* @param key - The state key
* @param value - The value to store (will be JSON serialized)
* @param description - Optional description of what this state stores
*/
async setState<T = unknown>(key: string, value: T, description?: string): Promise<void> {
try {
const db = dbService.getDb()
await db
.insert(appStateTable)
.values({
key,
value: value as any,
description
})
.onConflictDoUpdate({
target: appStateTable.key,
set: {
value: value as any,
description,
updatedAt: Date.now()
}
})
logger.debug('Saved app state', { key })
} catch (error) {
logger.error('Failed to save app state', error as Error, { key })
throw error
}
}
/**
* Delete app state by key
* @param key - The state key to delete
* @returns true if deleted, false if not found
*/
async deleteState(key: string): Promise<boolean> {
try {
const db = dbService.getDb()
const result = await db.delete(appStateTable).where(eq(appStateTable.key, key))
const deleted = result.rowsAffected > 0
logger.debug('Deleted app state', { key, deleted })
return deleted
} catch (error) {
logger.error('Failed to delete app state', error as Error, { key })
throw error
}
}
}
export const appStateService = AppStateService.getInstance()

View File

@ -0,0 +1,17 @@
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
// Create a new router instance
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export const AppRouter = () => {
return <RouterProvider router={router} />
}

View File

@ -0,0 +1,187 @@
// TODO demo component
import { cn } from '@cherrystudio/ui'
import { Link, Outlet, useLocation, useNavigate } from '@tanstack/react-router'
import { X } from 'lucide-react'
import { useEffect } from 'react'
import { useTabs } from '../../hooks/useTabs'
// Mock Sidebar component (Replace with actual one later)
const Sidebar = ({ onNavigate }: { onNavigate: (id: string) => void }) => {
// Helper to render a Sidebar Link that acts as a Tab Switcher
const SidebarItem = ({ to, title, id }: { to: string; title: string; id: string }) => (
<Link
to={to}
className="flex h-10 w-10 items-center justify-center rounded-md hover:bg-accent data-[status=active]:bg-primary/20 data-[status=active]:font-bold"
activeProps={{
'data-status': 'active'
}}
onClick={(e) => {
// Intercept the router navigation!
// We want to switch tabs, not just navigate within the current tab.
e.preventDefault()
onNavigate(id)
}}>
{title.slice(0, 1).toUpperCase() + title.slice(1, 3)}
</Link>
)
return (
<aside className="flex h-full w-16 flex-col items-center gap-4 border-r bg-muted/10 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/20 font-bold text-xs">Logo</div>
<SidebarItem to="/" title="Home" id="home" />
<SidebarItem to="/settings" title="Settings" id="settings" />
<div className="flex-1" />
<button type="button" className="flex h-10 w-10 items-center justify-center rounded-md hover:bg-accent">
User
</button>
</aside>
)
}
// Mock MinApp component (Replace with actual implementation)
const MinApp = ({ url }: { url: string }) => (
<div className="flex h-full w-full flex-col items-center justify-center bg-background">
<div className="mb-2 font-bold text-lg">Webview App</div>
<code className="rounded bg-muted p-2">{url}</code>
</div>
)
export const AppShell = () => {
const { tabs, activeTabId, setActiveTab, closeTab, addTab, updateTab } = useTabs()
const navigate = useNavigate()
const location = useLocation()
// 1. Sync Route -> Tab (Handle internal navigation & deep links)
useEffect(() => {
const currentPath = location.pathname
const activeTab = tabs.find((t) => t.id === activeTabId)
if (activeTab?.type === 'url' && activeTab.url !== currentPath) {
const existingTab = tabs.find((t) => t.type === 'url' && t.url === currentPath && t.id !== activeTabId)
if (existingTab) {
setActiveTab(existingTab.id)
} else {
// Sync URL changes back to DB
updateTab(activeTabId, { url: currentPath })
}
}
}, [location.pathname, tabs, activeTabId, setActiveTab, updateTab])
// 2. Sync Tab -> Route (Handle tab switching)
useEffect(() => {
const activeTab = tabs.find((t) => t.id === activeTabId)
if (!activeTab) return
if (activeTab.type === 'url') {
if (location.pathname !== activeTab.url) {
navigate({ to: activeTab.url })
}
}
}, [activeTabId, tabs, navigate, location.pathname])
const handleSidebarClick = (menuId: string) => {
let targetUrl = ''
let targetTitle = ''
switch (menuId) {
case 'home':
targetUrl = '/'
targetTitle = 'Home'
break
case 'settings':
targetUrl = '/settings'
targetTitle = 'Settings'
break
default:
return
}
const existingTab = tabs.find((t) => t.type === 'url' && t.url === targetUrl)
if (existingTab) {
setActiveTab(existingTab.id)
} else {
addTab({
id: `${menuId}-${Date.now()}`,
type: 'url',
url: targetUrl,
title: targetTitle
})
}
}
const activeTab = tabs.find((t) => t.id === activeTabId)
const isWebviewActive = activeTab?.type === 'webview'
return (
<div className="flex h-screen w-screen flex-row overflow-hidden bg-background text-foreground">
{/* Zone 1: Sidebar */}
<Sidebar onNavigate={handleSidebarClick} />
<div className="flex h-full min-w-0 flex-1 flex-col">
{/* Zone 2: Tab Bar */}
<header className="flex h-10 w-full items-center border-b bg-muted/5">
<div className="hide-scrollbar flex-1 overflow-x-auto">
<div className="flex h-full w-full items-center justify-start">
{tabs.map((tab) => (
<Link
key={tab.id}
to={tab.url}
onClick={() => setActiveTab(tab.id)}
className={cn(
'relative flex h-full min-w-[120px] max-w-[200px] items-center justify-between gap-2 border-border/40 border-r px-3 py-2 text-sm transition-colors hover:bg-muted/50',
tab.id === activeTabId ? 'bg-background shadow-sm' : 'bg-transparent opacity-70 hover:opacity-100'
)}>
<span className="truncate text-xs">{tab.title}</span>
<div
role="button"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
closeTab(tab.id)
}}
className="ml-1 cursor-pointer rounded-sm p-0.5 opacity-50 hover:bg-muted-foreground/20 hover:opacity-100">
<X className="size-3" />
</div>
</Link>
))}
</div>
</div>
</header>
{/* Zone 3: Content Area (Simplified Hybrid Architecture) */}
<main className="relative flex-1 overflow-hidden bg-background">
{/* Layer A: Standard Router Outlet */}
{/* Always rendered, but hidden if a webview is active. This keeps the Router alive. */}
<div
style={{
display: isWebviewActive ? 'none' : 'block',
height: '100%',
width: '100%'
}}>
<Outlet />
</div>
{/* Layer B: Webview Apps (Overlay) */}
{tabs.map((tab) => {
if (tab.type !== 'webview') return null
return (
<div
key={tab.id}
style={{
display: tab.id === activeTabId ? 'block' : 'none',
height: '100%',
width: '100%'
}}>
<MinApp url={tab.url} />
</div>
)
})}
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,107 @@
# Router Architecture & Tab System
## Overview
This document describes the routing and tab management architecture for Cherry Studio. The system implements a "Chrome-like" tabbed interface where every tab can be a distinct application state (Chat, Settings, MinApp/Webview).
The architecture is **Hybrid**, combining:
1. **TanStack Router (HashRouter)**: Handles URL parsing and standard React page rendering. Standard pages (Home, Settings) are re-mounted on tab switch but optimized via data caching.
2. **Webview Overlay System**: Manages persistent processes (Webviews) that live *outside* the router to ensure they are never destroyed during navigation.
3. **Bidirectional State Sync**: `AppShell` ensures the URL bar and the Tab Database (`app_state` table) are always in sync.
## Core Architecture
### 1. Hybrid Rendering
We use a "Single Router + Overlay" approach. We do **not** force `KeepAlive` for standard React pages, as it complicates the router logic significantly. Instead, we rely on TanStack Router's fast caching to make re-mounting feel instant.
* **Layer 1: Standard Router (The Outlet)**
* Always present in the DOM.
* Renders standard pages (Home, Settings, Native Chat).
* **Behavior**: When switching between standard tabs (e.g., Home -> Settings), the router navigates, unmounting the old component and mounting the new one.
* **Optimization**: Data loaders are cached, so "re-mounting" is cheap and fast.
* **Visibility**: Hidden via `display: none` if a Webview tab is active.
* **Layer 2: Webview Overlays**
* Rendered **outside** the Router.
* **Behavior**: These components are *never* unmounted as long as the tab is open.
* **Visibility**: Controlled purely by CSS (`display: block` / `none`).
* **Purpose**: To keep heavy processes (like MinApps or external websites) alive in the background.
### 2. State Synchronization (The "Listener" Pattern)
Since we use a single Router instance, we must manually sync the "Active Tab's URL" with the "Router's URL".
* **URL -> Database (Passive Sync)**:
* A `useEffect` hook in `AppShell` listens to `location.pathname`.
* If the URL changes (e.g., user navigates inside a Chat tab), we update the current tab's `url` field in the SQLite database.
* *Benefit*: Restores the exact sub-route (e.g., `/chat/session-123`) when the user comes back later.
* **Tab Switch -> Router (Active Navigation)**:
* When the user clicks a tab, we read its stored `url` from the database.
* We calling `navigate({ to: storedUrl })` to restore the view.
### 3. Data Management
* **Storage**: Tab data (`tabs`, `activeTabId`) is stored in SQLite (`app_state` table).
* **Sync**: `useTabs` hook uses SWR (`useQuery`) to sync frontend state with the database.
* **Optimistic Updates**: UI updates immediately, background sync handles persistence.
## Routing & Overlay Mapping
For detailed route tree definitions and component mappings, please refer to [Router Planning](./router-planning.md).
### Handling Webview Routes
The planning document mentions routes like `/apps/$appId` that may correspond to Webview applications. In our Hybrid Architecture, these are handled as follows:
1. **Router Layer**: The route `/apps/$appId` is still defined in TSR.
* Purpose: Maintains URL semantics and supports deep linking.
* Rendering: Renders a "shell" component or loading state.
2. **Overlay Layer**: `AppShell` detects that the current Tab type is `webview`.
* Behavior: Hides the Router's Outlet.
* Rendering: Displays the corresponding `<Webview />` instance in the Overlay layer.
This mechanism ensures that even Webview apps have standard URLs, providing a consistent navigation experience across the application.
## Key Components
### `AppShell` (`src/renderer/src/components/layout/AppShell.tsx`)
The coordinator that manages the two layers.
```tsx
return (
<div className="app-shell">
<Sidebar />
<div className="main-content">
<TabBar />
{/* Layer 1: Standard Router (Hidden if Webview is active) */}
<div style={{ display: isWebviewActive ? 'none' : 'block' }}>
<Outlet />
</div>
{/* Layer 2: Webview Overlays (Only for type='webview') */}
{tabs.map(tab => {
if (tab.type !== 'webview') return null;
return (
<div style={{ display: isActive ? 'block' : 'none' }}>
<Webview url={tab.url} />
</div>
)
})}
</div>
</div>
)
```
## Trade-offs
| Feature | Approach | Rationale |
| :--- | :--- | :--- |
| **Standard Pages** | Re-mount on switch | Simplicity. Reactivity problems with KeepAlive are avoided. TSR caching makes it fast. |
| **Webviews** | Keep-Alive (CSS Hide) | Essential. Reloading an external app/website is bad UX. |
| **Routing** | HashRouter | Native to Electron file system. Avoids history API complexities. |
| **URL Logic** | Single Source of Truth | The address bar always reflects the *active* tab. Background tabs are just state in DB. |

View File

@ -0,0 +1,347 @@
# Router Planning
> Version: v0.1.0
> Updated: 2025-11-25
> Status: Draft
## 1. Overview
This document defines the routing structure plan for migrating Cherry Studio from React Router to TanStack Router (TSR).
### 1.1 Core Interaction Model
```
┌─────────────────────────────────────────────────────────────────┐
│ Left Sidebar │ Top Tab Bar │
│ (Shortcuts) │ [Tab1] [Tab2] [Tab3] [+] │
│ ┌───────────┐ ├──────────────────────────────────────────┤
│ │ 💬 Chat │ │ │
│ │ ⚙️ Settings│ │ Content Area (Outlet) │
│ │ 📁 Files │ │ │
│ │ 📝 Notes │ │ Rendered based on active Tab's URL │
│ │ ... │ │ │
│ └───────────┘ │ │
└─────────────────────────────────────────────────────────────────┘
```
- **Left Sidebar**: Like a "bookmarks bar", stores shortcuts. Clicking navigates to the URL (may reuse existing Tab or create new Tab)
- **Top Tab Bar**: Manages multiple open pages, supports closing and switching
- **Content Area**: Rendered by TanStack Router's `<Outlet />`
---
## 2. Route Structure
### 2.1 Directory Structure
```
src/renderer/src/routes/
├── __root.tsx # Root route → AppShell
├── index.tsx # / → Welcome page or redirect (TBD)
├── chat/
│ ├── route.tsx # /chat layout: sidebar + <Outlet/>
│ ├── index.tsx # /chat → Empty state (no topic selected)
│ └── $assistantId/
│ ├── route.tsx # /chat/$assistantId layout (optional)
│ ├── index.tsx # /chat/$assistantId → Assistant home (optional)
│ └── $topicId.tsx # /chat/$assistantId/$topicId → Chat view
├── settings/
│ ├── route.tsx # /settings layout: menu + <Outlet/>
│ ├── index.tsx # /settings → Redirect to default sub-page
│ ├── provider.tsx # /settings/provider
│ ├── model.tsx # /settings/model
│ ├── general.tsx # /settings/general
│ ├── display.tsx # /settings/display
│ ├── data.tsx # /settings/data
│ ├── mcp.tsx # /settings/mcp
│ ├── shortcut.tsx # /settings/shortcut
│ └── about.tsx # /settings/about
├── knowledge/
│ ├── route.tsx # /knowledge layout
│ ├── index.tsx # /knowledge → Knowledge base list
│ └── $baseId.tsx # /knowledge/$baseId → Knowledge base detail
├── notes/
│ ├── route.tsx # /notes layout: tree sidebar + <Outlet/>
│ ├── index.tsx # /notes → Empty state
│ └── $noteId.tsx # /notes/$noteId → Editor
├── apps/
│ ├── route.tsx # /apps layout
│ ├── index.tsx # /apps → App list
│ └── $appId.tsx # /apps/$appId → App detail (possibly Webview)
├── paintings/
│ ├── route.tsx # /paintings layout: provider select + <Outlet/>
│ ├── index.tsx # /paintings → Redirect to default provider
│ ├── zhipu.tsx # /paintings/zhipu → Zhipu painting
│ ├── aihubmix.tsx # /paintings/aihubmix → Aihubmix
│ ├── silicon.tsx # /paintings/silicon → Silicon Flow
│ ├── dmxapi.tsx # /paintings/dmxapi → Dmxapi
│ ├── tokenflux.tsx # /paintings/tokenflux → TokenFlux
│ ├── ovms.tsx # /paintings/ovms → OVMS
│ └── $providerId.tsx # /paintings/$providerId → Dynamic NewApi provider
├── files.tsx # /files → File management
├── translate.tsx # /translate → Translation
├── store.tsx # /store → App store
└── launchpad.tsx # /launchpad → Launchpad
```
### 2.2 Route Table
| Route | Component | Loader Data | Description |
|-------|-----------|-------------|-------------|
| `/` | `WelcomePage` | - | Welcome page or redirect (TBD) |
| `/chat` | `ChatLayout` | Assistants, Topics | Chat layout layer |
| `/chat/$assistantId/$topicId` | `ChatView` | Topic detail, Messages | Chat main view |
| `/settings` | `SettingsLayout` | - | Settings layout layer |
| `/settings/provider` | `ProviderSettings` | Provider list | Provider settings |
| `/settings/model` | `ModelSettings` | Model list | Model settings |
| `/settings/*` | `*Settings` | Respective data | Other settings pages |
| `/knowledge` | `KnowledgeLayout` | Knowledge bases | Knowledge layout |
| `/knowledge/$baseId` | `KnowledgeDetail` | Knowledge detail | Knowledge detail page |
| `/notes` | `NotesLayout` | Notes tree | Notes layout |
| `/notes/$noteId` | `NotesEditor` | Note content | Notes editor |
| `/apps` | `AppsLayout` | App list | Apps layout |
| `/apps/$appId` | `AppDetail` | App detail | App detail/Webview |
| `/paintings` | `PaintingsLayout` | Provider list | Paintings layout layer |
| `/paintings/zhipu` | `ZhipuPage` | - | Zhipu painting |
| `/paintings/aihubmix` | `AihubmixPage` | - | Aihubmix painting |
| `/paintings/silicon` | `SiliconPage` | - | Silicon Flow painting |
| `/paintings/dmxapi` | `DmxapiPage` | - | Dmxapi painting |
| `/paintings/tokenflux` | `TokenFluxPage` | - | TokenFlux painting |
| `/paintings/ovms` | `OvmsPage` | - | OVMS painting |
| `/paintings/$providerId` | `NewApiPage` | - | Dynamic NewApi provider |
| `/files` | `FilesPage` | File list | File management |
| `/translate` | `TranslatePage` | - | Translation page |
| `/store` | `StorePage` | Store data | App store |
| `/launchpad` | `LaunchpadPage` | - | Launchpad |
---
## 3. Chat Route Design
### 3.1 URL Structure
```
/chat/$assistantId/$topicId
│ │
│ └── Topic ID (conversation ID)
└── Assistant ID
```
**Examples**:
- `/chat` → Chat home (sidebar + empty state)
- `/chat/assistant-1` → Assistant 1's home (optional, may redirect to first topic)
- `/chat/assistant-1/topic-123` → Chat view for topic 123 under assistant 1
### 3.2 Component Structure
```tsx
// routes/chat/route.tsx
export const Route = createFileRoute('/chat')({
component: ChatLayout,
loader: async () => ({
assistants: await fetchAssistants(),
topics: await fetchTopics()
}),
staleTime: 30_000,
})
function ChatLayout() {
const data = Route.useLoaderData()
return (
<div className="flex h-full">
{/* Sidebar: Assistant list + Topic list */}
<ChatSidebar assistants={data.assistants} topics={data.topics} />
{/* Chat content area */}
<div className="flex-1 min-w-0">
<Outlet />
</div>
</div>
)
}
```
```tsx
// routes/chat/$assistantId/$topicId.tsx
export const Route = createFileRoute('/chat/$assistantId/$topicId')({
component: ChatView,
loader: async ({ params }) => ({
topic: await fetchTopic(params.topicId),
messages: await fetchMessages(params.topicId)
}),
staleTime: 10_000,
})
function ChatView() {
const { topic, messages } = Route.useLoaderData()
const { assistantId, topicId } = Route.useParams()
return (
<div className="flex flex-col h-full">
<ChatNavbar topic={topic} />
<Messages messages={messages} />
<Inputbar topicId={topicId} assistantId={assistantId} />
</div>
)
}
```
### 3.3 Data Flow
```
1. User clicks topic in sidebar
2. navigate({ to: '/chat/$assistantId/$topicId' })
3. TSR matches route, checks loader cache
4. Cache hit → Render directly
Cache miss → Execute loader, fetch data
5. ChatLayout does not re-render (parent route data cached)
6. Only ChatView updates (child route data changed)
```
---
## 4. Settings Route Design
### 4.1 Sub-page List
| Route | Component | Existing File |
|-------|-----------|---------------|
| `/settings/provider` | `ProviderSettings` | `ProviderSettings/` |
| `/settings/model` | `ModelSettings` | `ModelSettings/` |
| `/settings/general` | `GeneralSettings` | `GeneralSettings.tsx` |
| `/settings/display` | `DisplaySettings` | `DisplaySettings.tsx` |
| `/settings/data` | `DataSettings` | `DataSettings/` |
| `/settings/mcp` | `MCPSettings` | `MCPSettings/` |
| `/settings/websearch` | `WebSearchSettings` | `WebSearchSettings/` |
| `/settings/memory` | `MemorySettings` | `MemorySettings/` |
| `/settings/shortcut` | `ShortcutSettings` | `ShortcutSettings.tsx` |
| `/settings/quickassistant` | `QuickAssistantSettings` | `QuickAssistantSettings.tsx` |
| `/settings/about` | `AboutSettings` | `AboutSettings.tsx` |
### 4.2 Layout Structure
```tsx
// routes/settings/route.tsx
function SettingsLayout() {
return (
<div className="flex h-full">
{/* Left menu */}
<SettingsMenu />
{/* Right content */}
<div className="flex-1 overflow-auto">
<Outlet />
</div>
</div>
)
}
```
---
## 5. Paintings Route Design
### 5.1 URL Structure
```
/paintings/$providerId
└── Provider ID (zhipu, aihubmix, silicon, dmxapi, tokenflux, ovms, or dynamic NewApi provider)
```
**Examples**:
- `/paintings` → Redirect to user's default painting provider
- `/paintings/zhipu` → Zhipu painting page
- `/paintings/aihubmix` → Aihubmix painting page
- `/paintings/my-custom-provider` → User's custom NewApi provider
### 5.2 Provider List
| Provider ID | Component | Description |
|-------------|-----------|-------------|
| `zhipu` | `ZhipuPage` | Zhipu AI Painting |
| `aihubmix` | `AihubmixPage` | Aihubmix Aggregation |
| `silicon` | `SiliconPage` | Silicon Flow |
| `dmxapi` | `DmxapiPage` | Dmxapi |
| `tokenflux` | `TokenFluxPage` | TokenFlux |
| `ovms` | `OvmsPage` | OVMS (Local Inference) |
| `$providerId` | `NewApiPage` | Dynamic NewApi Provider |
### 5.3 Component Structure
```tsx
// routes/paintings/route.tsx
export const Route = createFileRoute('/paintings')({
component: PaintingsLayout,
loader: async () => ({
providers: await fetchPaintingProviders(),
defaultProvider: await getDefaultPaintingProvider()
}),
})
function PaintingsLayout() {
const { providers } = Route.useLoaderData()
return (
<div className="flex flex-col h-full">
{/* Provider selector */}
<ProviderSelect providers={providers} />
{/* Painting content area */}
<div className="flex-1">
<Outlet />
</div>
</div>
)
}
```
### 5.4 Special Handling
- **OVMS Provider**: Only shown in options when local OVMS service is running
- **Dynamic Providers**: Custom providers added by users via NewApi, captured using `$providerId`
---
## 6. Component Mapping
| New Route Component | Existing Component | Migration Strategy |
|---------------------|-------------------|-------------------|
| `ChatLayout` | `HomePage.tsx` | Extract sidebar logic |
| `ChatSidebar` | `HomeTabs/index.tsx` | Rename, adjust props |
| `ChatView` | `Chat.tsx` | Keep unchanged, adjust data fetching |
| `SettingsLayout` | `SettingsPage.tsx` | Extract layout logic |
| `NotesLayout` | `NotesSidebar.tsx` | Extract as layout component |
| `NotesEditor` | `NotesEditor.tsx` | Keep unchanged |
---
## 7. Open Questions
- [ ] `/` home behavior: Redirect to `/chat` or standalone welcome page?
- [ ] Does `/chat/$assistantId` need a dedicated page? Or redirect to first topic directly?
- [ ] Left sidebar interaction: Always create new Tab on click? Or reuse existing Tab?
- [ ] Tab bar UI details: Close button position, drag-to-reorder, context menu, etc.
---
## 8. Changelog
| Version | Date | Changes |
|---------|------|---------|
| v0.1.0 | 2025-11-25 | Initial version |

View File

@ -2,6 +2,7 @@ import type { BodyForPath, QueryParamsForPath, ResponseForPath } from '@shared/d
import type { ConcreteApiPaths } from '@shared/data/api/apiSchemas'
import type { PaginatedResponse } from '@shared/data/api/apiTypes'
import { useState } from 'react'
import type { KeyedMutator } from 'swr'
import useSWR, { useSWRConfig } from 'swr'
import useSWRMutation from 'swr/mutation'
@ -139,6 +140,8 @@ export function useQuery<TPath extends ConcreteApiPaths>(
error?: Error
/** Function to manually refetch data */
refetch: () => void
/** SWR mutate function for optimistic updates */
mutate: KeyedMutator<ResponseForPath<TPath, 'GET'>>
} {
// Internal type conversion for SWR compatibility
const key = options?.enabled !== false ? buildSWRKey(path, options?.query as Record<string, any>) : null
@ -160,7 +163,8 @@ export function useQuery<TPath extends ConcreteApiPaths>(
data,
loading: isLoading || isValidating,
error: error as Error | undefined,
refetch
refetch,
mutate
}
}

View File

@ -0,0 +1,122 @@
import { loggerService } from '@logger'
import { useCallback, useMemo } from 'react'
import { useMutation, useQuery } from '../data/hooks/useDataApi'
const logger = loggerService.withContext('useTabs')
export type TabType = 'webview' | 'url' | 'browser'
export interface Tab {
id: string
type: TabType
url: string
title: string
icon?: string
isKeepAlive?: boolean
metadata?: Record<string, any>
}
interface TabsState {
tabs: Tab[]
activeTabId: string
}
const TABS_STORAGE_KEY = 'tabs_state'
const DEFAULT_STATE: TabsState = { tabs: [], activeTabId: '' }
export function useTabs() {
// Load state from DB
// We cast the path because we haven't fully updated the concrete path types globally yet
const {
data: tabsState,
mutate: mutateState,
loading: isLoading
} = useQuery(`/app/state/${TABS_STORAGE_KEY}` as any, {
swrOptions: {
revalidateOnFocus: false,
fallbackData: DEFAULT_STATE
}
})
// Ensure we always have a valid object structure
const currentState: TabsState = useMemo(
() => (tabsState && typeof tabsState === 'object' ? (tabsState as TabsState) : DEFAULT_STATE),
[tabsState]
)
const tabs = useMemo(() => (Array.isArray(currentState.tabs) ? currentState.tabs : []), [currentState.tabs])
const activeTabId = currentState.activeTabId || ''
// Mutation for saving
const saveMutation = useMutation('PUT', `/app/state/${TABS_STORAGE_KEY}` as any)
// Unified update helper
const updateState = useCallback(
async (newState: TabsState) => {
// 1. Optimistic update local cache
await mutateState(newState, { revalidate: false })
// 2. Sync to DB
saveMutation.mutate({ body: newState }).catch((err) => logger.error('Failed to save tabs state:', err))
},
[mutateState, saveMutation]
)
const addTab = useCallback(
(tab: Tab) => {
const exists = tabs.find((t) => t.id === tab.id)
if (exists) {
updateState({ ...currentState, activeTabId: tab.id })
return
}
const newTabs = [...tabs, tab]
updateState({ tabs: newTabs, activeTabId: tab.id })
},
[tabs, currentState, updateState]
)
const closeTab = useCallback(
(id: string) => {
const newTabs = tabs.filter((t) => t.id !== id)
let newActiveId = activeTabId
if (activeTabId === id) {
// Activate adjacent tab
const index = tabs.findIndex((t) => t.id === id)
// Try to go left, then right
const nextTab = newTabs[index - 1] || newTabs[index]
newActiveId = nextTab ? nextTab.id : ''
}
updateState({ tabs: newTabs, activeTabId: newActiveId })
},
[tabs, activeTabId, updateState]
)
const setActiveTab = useCallback(
(id: string) => {
if (id !== activeTabId) {
updateState({ ...currentState, activeTabId: id })
}
},
[activeTabId, currentState, updateState]
)
const updateTab = useCallback(
(id: string, updates: Partial<Tab>) => {
const newTabs = tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
updateState({ ...currentState, tabs: newTabs })
},
[tabs, currentState, updateState]
)
return {
tabs,
activeTabId,
isLoading,
addTab,
closeTab,
setActiveTab,
updateTab
}
}

View File

@ -0,0 +1,55 @@
// @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 IndexRouteImport } from './routes/index'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute
}
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()

View File

@ -0,0 +1,7 @@
import { createRootRoute } from '@tanstack/react-router'
import { AppShell } from '../components/layout/AppShell'
export const Route = createRootRoute({
component: () => <AppShell />
})

View File

@ -0,0 +1,14 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: Index
})
function Index() {
return (
<div style={{ padding: 20 }}>
<h3>Welcome to Cherry Studio!</h3>
<p>Select a tab to start.</p>
</div>
)
}

459
yarn.lock
View File

@ -1545,7 +1545,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:^7.21.3":
"@babel/core@npm:^7.21.3, @babel/core@npm:^7.23.7, @babel/core@npm:^7.27.4":
version: 7.28.5
resolution: "@babel/core@npm:7.28.5"
dependencies:
@ -1591,6 +1591,19 @@ __metadata:
languageName: node
linkType: hard
"@babel/generator@npm:^7.27.5, @babel/generator@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/generator@npm:7.28.5"
dependencies:
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2"
checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
languageName: node
linkType: hard
"@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/generator@npm:7.28.3"
@ -1604,16 +1617,12 @@ __metadata:
languageName: node
linkType: hard
"@babel/generator@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/generator@npm:7.28.5"
"@babel/helper-annotate-as-pure@npm:^7.27.3":
version: 7.27.3
resolution: "@babel/helper-annotate-as-pure@npm:7.27.3"
dependencies:
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2"
checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
"@babel/types": "npm:^7.27.3"
checksum: 10c0/94996ce0a05b7229f956033e6dcd69393db2b0886d0db6aff41e704390402b8cdcca11f61449cb4f86cfd9e61b5ad3a73e4fa661eeed7846b125bd1c33dbc633
languageName: node
linkType: hard
@ -1630,6 +1639,23 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-create-class-features-plugin@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/helper-create-class-features-plugin@npm:7.28.5"
dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.27.3"
"@babel/helper-member-expression-to-functions": "npm:^7.28.5"
"@babel/helper-optimise-call-expression": "npm:^7.27.1"
"@babel/helper-replace-supers": "npm:^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
"@babel/traverse": "npm:^7.28.5"
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10c0/786a6514efcf4514aaad85beed419b9184d059f4c9a9a95108f320142764999827252a851f7071de19f29424d369616573ecbaa347f1ce23fb12fc6827d9ff56
languageName: node
linkType: hard
"@babel/helper-globals@npm:^7.28.0":
version: 7.28.0
resolution: "@babel/helper-globals@npm:7.28.0"
@ -1637,6 +1663,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-member-expression-to-functions@npm:^7.27.1, @babel/helper-member-expression-to-functions@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5"
dependencies:
"@babel/traverse": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814
languageName: node
linkType: hard
"@babel/helper-module-imports@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-module-imports@npm:7.27.1"
@ -1647,7 +1683,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-transforms@npm:^7.28.3":
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/helper-module-transforms@npm:7.28.3"
dependencies:
@ -1660,6 +1696,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-optimise-call-expression@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-optimise-call-expression@npm:7.27.1"
dependencies:
"@babel/types": "npm:^7.27.1"
checksum: 10c0/6b861e7fcf6031b9c9fc2de3cd6c005e94a459d6caf3621d93346b52774925800ca29d4f64595a5ceacf4d161eb0d27649ae385110ed69491d9776686fa488e6
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
@ -1667,6 +1712,29 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-replace-supers@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-replace-supers@npm:7.27.1"
dependencies:
"@babel/helper-member-expression-to-functions": "npm:^7.27.1"
"@babel/helper-optimise-call-expression": "npm:^7.27.1"
"@babel/traverse": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10c0/4f2eaaf5fcc196580221a7ccd0f8873447b5d52745ad4096418f6101a1d2e712e9f93722c9a32bc9769a1dc197e001f60d6f5438d4dfde4b9c6a9e4df719354c
languageName: node
linkType: hard
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1"
dependencies:
"@babel/traverse": "npm:^7.27.1"
"@babel/types": "npm:^7.27.1"
checksum: 10c0/f625013bcdea422c470223a2614e90d2c1cc9d832e97f32ca1b4f82b34bb4aa67c3904cb4b116375d3b5b753acfb3951ed50835a1e832e7225295c7b0c24dff7
languageName: node
linkType: hard
"@babel/helper-string-parser@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-string-parser@npm:7.27.1"
@ -1705,7 +1773,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4, @babel/parser@npm:^7.28.5":
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.6, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4, @babel/parser@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/parser@npm:7.28.5"
dependencies:
@ -1727,6 +1795,28 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-syntax-jsx@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-syntax-jsx@npm:7.27.1"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/bc5afe6a458d5f0492c02a54ad98c5756a0c13bd6d20609aae65acd560a9e141b0876da5f358dce34ea136f271c1016df58b461184d7ae9c4321e0f98588bc84
languageName: node
linkType: hard
"@babel/plugin-syntax-typescript@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-syntax-typescript@npm:7.27.1"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/11589b4c89c66ef02d57bf56c6246267851ec0c361f58929327dc3e070b0dab644be625bbe7fb4c4df30c3634bfdfe31244e1f517be397d2def1487dbbe3c37d
languageName: node
linkType: hard
"@babel/plugin-transform-arrow-functions@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1"
@ -1738,6 +1828,48 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-modules-commonjs@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1"
dependencies:
"@babel/helper-module-transforms": "npm:^7.27.1"
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/4def972dcd23375a266ea1189115a4ff61744b2c9366fc1de648b3fab2c650faf1a94092de93a33ff18858d2e6c4dddeeee5384cb42ba0129baeab01a5cdf1e2
languageName: node
linkType: hard
"@babel/plugin-transform-typescript@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-typescript@npm:7.28.5"
dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.27.3"
"@babel/helper-create-class-features-plugin": "npm:^7.28.5"
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
"@babel/plugin-syntax-typescript": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/09e574ba5462e56452b4ceecae65e53c8e697a2d3559ce5d210bed10ac28a18aa69377e7550c30520eb29b40c417ee61997d5d58112657f22983244b78915a7c
languageName: node
linkType: hard
"@babel/preset-typescript@npm:^7.27.1":
version: 7.28.5
resolution: "@babel/preset-typescript@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-validator-option": "npm:^7.27.1"
"@babel/plugin-syntax-jsx": "npm:^7.27.1"
"@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
"@babel/plugin-transform-typescript": "npm:^7.28.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/b3d55548854c105085dd80f638147aa8295bc186d70492289242d6c857cb03a6c61ec15186440ea10ed4a71cdde7d495f5eb3feda46273f36b0ac926e8409629
languageName: node
linkType: hard
"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.7":
version: 7.28.3
resolution: "@babel/runtime@npm:7.28.3"
@ -1763,6 +1895,21 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.23.7, @babel/traverse@npm:^7.27.7, @babel/traverse@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/traverse@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.5"
debug: "npm:^4.3.1"
checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
languageName: node
linkType: hard
"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/traverse@npm:7.28.4"
@ -1778,21 +1925,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/traverse@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.5"
debug: "npm:^4.3.1"
checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/types@npm:7.28.4"
@ -1803,7 +1935,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.21.3, @babel/types@npm:^7.28.5":
"@babel/types@npm:^7.21.3, @babel/types@npm:^7.23.6, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
@ -11016,6 +11148,13 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/history@npm:1.139.0":
version: 1.139.0
resolution: "@tanstack/history@npm:1.139.0"
checksum: 10c0/000fe41d3c3d7f0384e74fcfb1ecda25800906220925d1ab715e4fad7dab081e81c738238a3c09bfb1203ffd4ab0e1f24da1384ded322a154b9eba58405e3e90
languageName: node
linkType: hard
"@tanstack/query-core@npm:5.85.5":
version: 5.85.5
resolution: "@tanstack/query-core@npm:5.85.5"
@ -11034,6 +11173,36 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-router@npm:^1.139.3":
version: 1.139.3
resolution: "@tanstack/react-router@npm:1.139.3"
dependencies:
"@tanstack/history": "npm:1.139.0"
"@tanstack/react-store": "npm:^0.8.0"
"@tanstack/router-core": "npm:1.139.3"
isbot: "npm:^5.1.22"
tiny-invariant: "npm:^1.3.3"
tiny-warning: "npm:^1.0.3"
peerDependencies:
react: ">=18.0.0 || >=19.0.0"
react-dom: ">=18.0.0 || >=19.0.0"
checksum: 10c0/1a145ee628ab43c5ce326dc1e5068508b9b71c3570723b2acee6a11d0487221f704489f9f73a93903cb1cba31985fdadf59922ca2148bf91246cbeb71d64aab9
languageName: node
linkType: hard
"@tanstack/react-store@npm:^0.8.0":
version: 0.8.0
resolution: "@tanstack/react-store@npm:0.8.0"
dependencies:
"@tanstack/store": "npm:0.8.0"
use-sync-external-store: "npm:^1.6.0"
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
checksum: 10c0/ecf7ad81d97810336d0a808a41442f235a444e98599c6e7e026efd3c4360548b84af9a23612f1d0da85e32a4d9e207632b2ee2cec6f635109a256209caa3bc59
languageName: node
linkType: hard
"@tanstack/react-virtual@npm:3.11.3":
version: 3.11.3
resolution: "@tanstack/react-virtual@npm:3.11.3"
@ -11058,6 +11227,99 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/router-core@npm:1.139.3":
version: 1.139.3
resolution: "@tanstack/router-core@npm:1.139.3"
dependencies:
"@tanstack/history": "npm:1.139.0"
"@tanstack/store": "npm:^0.8.0"
cookie-es: "npm:^2.0.0"
seroval: "npm:^1.4.0"
seroval-plugins: "npm:^1.4.0"
tiny-invariant: "npm:^1.3.3"
tiny-warning: "npm:^1.0.3"
checksum: 10c0/1e342c39cb8a9e437671aeaa60c82c11530cd3ae0523983585f7c8d332b5ca471aa73d15ffd242cd4b2c73ecebeee7db8dc1b19cbfc3c211253782df9e4ac9fc
languageName: node
linkType: hard
"@tanstack/router-generator@npm:1.139.3":
version: 1.139.3
resolution: "@tanstack/router-generator@npm:1.139.3"
dependencies:
"@tanstack/router-core": "npm:1.139.3"
"@tanstack/router-utils": "npm:1.139.0"
"@tanstack/virtual-file-routes": "npm:1.139.0"
prettier: "npm:^3.5.0"
recast: "npm:^0.23.11"
source-map: "npm:^0.7.4"
tsx: "npm:^4.19.2"
zod: "npm:^3.24.2"
checksum: 10c0/6ee595fc7b3da75016129bbf65d8ee1ec50e3c5d96dbabf56718c5ee3332c4f660aa738a7dd8f8207d6aad4e71f3c6c51572f17192db12ada668dc5e491d6757
languageName: node
linkType: hard
"@tanstack/router-plugin@npm:^1.139.3":
version: 1.139.3
resolution: "@tanstack/router-plugin@npm:1.139.3"
dependencies:
"@babel/core": "npm:^7.27.7"
"@babel/plugin-syntax-jsx": "npm:^7.27.1"
"@babel/plugin-syntax-typescript": "npm:^7.27.1"
"@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.27.7"
"@babel/types": "npm:^7.27.7"
"@tanstack/router-core": "npm:1.139.3"
"@tanstack/router-generator": "npm:1.139.3"
"@tanstack/router-utils": "npm:1.139.0"
"@tanstack/virtual-file-routes": "npm:1.139.0"
babel-dead-code-elimination: "npm:^1.0.10"
chokidar: "npm:^3.6.0"
unplugin: "npm:^2.1.2"
zod: "npm:^3.24.2"
peerDependencies:
"@rsbuild/core": ">=1.0.2"
"@tanstack/react-router": ^1.139.3
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
checksum: 10c0/5795a880e65b27168fb703aff7254955787edde438fcfb381ddd201d74974806880164785b56f055a3c4186331a71ea0ccfa6f1474351cdeaf2608d10f13d2f9
languageName: node
linkType: hard
"@tanstack/router-utils@npm:1.139.0":
version: 1.139.0
resolution: "@tanstack/router-utils@npm:1.139.0"
dependencies:
"@babel/core": "npm:^7.27.4"
"@babel/generator": "npm:^7.27.5"
"@babel/parser": "npm:^7.27.5"
"@babel/preset-typescript": "npm:^7.27.1"
ansis: "npm:^4.1.0"
diff: "npm:^8.0.2"
pathe: "npm:^2.0.3"
tinyglobby: "npm:^0.2.15"
checksum: 10c0/cb8477ab32f16881b8b92db952d0ab343cb77704ada953e422cee4543dc31aa5efae31295d415ff7deea8f9fadd19ad1a88e26d5f3e0d30a2f6b4099e82d19c9
languageName: node
linkType: hard
"@tanstack/store@npm:0.8.0, @tanstack/store@npm:^0.8.0":
version: 0.8.0
resolution: "@tanstack/store@npm:0.8.0"
checksum: 10c0/71841a7a7653f744bdea457d2c41768b8d5e5aed1d5ff22bd068e28ced9bf658208c730963809c2223b26b753e19da987c0d98acb7c543abd97de14e0d58991f
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.11.3":
version: 3.11.3
resolution: "@tanstack/virtual-core@npm:3.11.3"
@ -11072,6 +11334,13 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/virtual-file-routes@npm:1.139.0":
version: 1.139.0
resolution: "@tanstack/virtual-file-routes@npm:1.139.0"
checksum: 10c0/abb8173520e133f3e4d8d2eb356059ce0fbc6aa64d55e358cbc810cc323c8848fbd3adf512095bafaf69e7bc01ee588ca5a345d361c42452b8eb41239ce5ed73
languageName: node
linkType: hard
"@testing-library/dom@npm:^10.4.0":
version: 10.4.0
resolution: "@testing-library/dom@npm:10.4.0"
@ -12384,12 +12653,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:^19.0.4":
version: 19.1.2
resolution: "@types/react-dom@npm:19.1.2"
"@types/react-dom@npm:^19.0.4, @types/react-dom@npm:^19.2.3":
version: 19.2.3
resolution: "@types/react-dom@npm:19.2.3"
peerDependencies:
"@types/react": ^19.0.0
checksum: 10c0/100c341cacba9ec8ae1d47ee051072a3450e9573bf8eeb7262490e341cb246ea0f95a07a1f2077e61cf92648f812a0324c602fcd811bd87b7ce41db2811510cd
"@types/react": ^19.2.0
checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1
languageName: node
linkType: hard
@ -12420,21 +12689,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:*":
version: 19.1.12
resolution: "@types/react@npm:19.1.12"
"@types/react@npm:*, @types/react@npm:^19.0.12, @types/react@npm:^19.2.7":
version: 19.2.7
resolution: "@types/react@npm:19.2.7"
dependencies:
csstype: "npm:^3.0.2"
checksum: 10c0/e35912b43da0caaab5252444bab87a31ca22950cde2822b3b3dc32e39c2d42dad1a4cf7b5dde9783aa2d007f0b2cba6ab9563fc6d2dbcaaa833b35178118767c
languageName: node
linkType: hard
"@types/react@npm:^19.0.12":
version: 19.1.2
resolution: "@types/react@npm:19.1.2"
dependencies:
csstype: "npm:^3.0.2"
checksum: 10c0/76ffe71395c713d4adc3c759465012d3c956db00af35ab7c6d0d91bd07b274b7ce69caa0478c0760311587bd1e38c78ffc9688ebc629f2b266682a19d8750947
csstype: "npm:^3.2.2"
checksum: 10c0/a7b75f1f9fcb34badd6f84098be5e35a0aeca614bc91f93d2698664c0b2ba5ad128422bd470ada598238cebe4f9e604a752aead7dc6f5a92261d0c7f9b27cfd1
languageName: node
linkType: hard
@ -13738,7 +13998,9 @@ __metadata:
"@swc/plugin-styled-components": "npm:^8.0.4"
"@tailwindcss/vite": "npm:^4.1.13"
"@tanstack/react-query": "npm:^5.85.5"
"@tanstack/react-router": "npm:^1.139.3"
"@tanstack/react-virtual": "npm:^3.13.12"
"@tanstack/router-plugin": "npm:^1.139.3"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/jest-dom": "npm:^6.6.3"
"@testing-library/react": "npm:^16.3.0"
@ -13776,8 +14038,8 @@ __metadata:
"@types/mime-types": "npm:^3"
"@types/node": "npm:^22.17.1"
"@types/pako": "npm:^1.0.2"
"@types/react": "npm:^19.0.12"
"@types/react-dom": "npm:^19.0.4"
"@types/react": "npm:^19.2.7"
"@types/react-dom": "npm:^19.2.3"
"@types/react-infinite-scroll-component": "npm:^5.0.0"
"@types/react-transition-group": "npm:^4.4.12"
"@types/react-window": "npm:^1"
@ -14641,6 +14903,18 @@ __metadata:
languageName: node
linkType: hard
"babel-dead-code-elimination@npm:^1.0.10":
version: 1.0.10
resolution: "babel-dead-code-elimination@npm:1.0.10"
dependencies:
"@babel/core": "npm:^7.23.7"
"@babel/parser": "npm:^7.23.6"
"@babel/traverse": "npm:^7.23.7"
"@babel/types": "npm:^7.23.6"
checksum: 10c0/9503662f28cf8f86e7a27c5cc1fa63fc556100cd3bc6f1a4382aa8e9c6df54b15d2e0fcc073016f315d26a9e4004bc4d70829a395f056172b8f9240314da8973
languageName: node
linkType: hard
"bail@npm:^1.0.0":
version: 1.0.5
resolution: "bail@npm:1.0.5"
@ -15348,7 +15622,7 @@ __metadata:
languageName: node
linkType: hard
"chokidar@npm:^3.5.2":
"chokidar@npm:^3.5.2, chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
@ -15947,6 +16221,13 @@ __metadata:
languageName: node
linkType: hard
"cookie-es@npm:^2.0.0":
version: 2.0.0
resolution: "cookie-es@npm:2.0.0"
checksum: 10c0/3b2459030a5ad2bc715aeb27a32f274340670bfc5031ac29e1fba804212517411bb617880d3fe66ace2b64dfb28f3049e2d1ff40d4bec342154ccdd124deaeaa
languageName: node
linkType: hard
"cookie-signature@npm:^1.2.1":
version: 1.2.2
resolution: "cookie-signature@npm:1.2.2"
@ -16198,6 +16479,13 @@ __metadata:
languageName: node
linkType: hard
"csstype@npm:^3.2.2":
version: 3.2.3
resolution: "csstype@npm:3.2.3"
checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
languageName: node
linkType: hard
"csv-parse@npm:^5.6.0":
version: 5.6.0
resolution: "csv-parse@npm:5.6.0"
@ -20602,6 +20890,13 @@ __metadata:
languageName: node
linkType: hard
"isbot@npm:^5.1.22":
version: 5.1.32
resolution: "isbot@npm:5.1.32"
checksum: 10c0/e5aa9c5c92dae4879cf49956797c46ef77fa919230183cd6254628667ca5e22f15b24bc4d63b0e88cb96da3d7a51e33f847ef7114fa542e3e066f78178c8d97e
languageName: node
linkType: hard
"isexe@npm:^2.0.0":
version: 2.0.0
resolution: "isexe@npm:2.0.0"
@ -24725,6 +25020,15 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^3.5.0":
version: 3.6.2
resolution: "prettier@npm:3.6.2"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812
languageName: node
linkType: hard
"pretty-format@npm:^27.0.2":
version: 27.5.1
resolution: "pretty-format@npm:27.5.1"
@ -26153,7 +26457,7 @@ __metadata:
languageName: node
linkType: hard
"recast@npm:^0.23.5":
"recast@npm:^0.23.11, recast@npm:^0.23.5":
version: 0.23.11
resolution: "recast@npm:0.23.11"
dependencies:
@ -27197,6 +27501,22 @@ __metadata:
languageName: node
linkType: hard
"seroval-plugins@npm:^1.4.0":
version: 1.4.0
resolution: "seroval-plugins@npm:1.4.0"
peerDependencies:
seroval: ^1.0
checksum: 10c0/d774b8a23bec45f1fefe314e38e26d2fffc0733ad50253a760a10f46cbb0be3a28ed9fcf60aadc0b3f1d2873f4118453a47e84145e858736944dbcd93b42437e
languageName: node
linkType: hard
"seroval@npm:^1.4.0":
version: 1.4.0
resolution: "seroval@npm:1.4.0"
checksum: 10c0/020262db5572c16ae5d22ecefa089112a0b1b9a9c78229dbc9c6059c172ed7f0b5005c7990b80714ff8638ac86274195c2084537e0c2a9178690acacff4b705f
languageName: node
linkType: hard
"serve-static@npm:^2.2.0":
version: 2.2.0
resolution: "serve-static@npm:2.2.0"
@ -28500,6 +28820,13 @@ __metadata:
languageName: node
linkType: hard
"tiny-warning@npm:^1.0.3":
version: 1.0.3
resolution: "tiny-warning@npm:1.0.3"
checksum: 10c0/ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa
languageName: node
linkType: hard
"tinybench@npm:^2.9.0":
version: 2.9.0
resolution: "tinybench@npm:2.9.0"
@ -28929,7 +29256,7 @@ __metadata:
languageName: node
linkType: hard
"tsx@npm:^4.20.3, tsx@npm:^4.20.6":
"tsx@npm:^4.19.2, tsx@npm:^4.20.3, tsx@npm:^4.20.6":
version: 4.20.6
resolution: "tsx@npm:4.20.6"
dependencies:
@ -29348,6 +29675,18 @@ __metadata:
languageName: node
linkType: hard
"unplugin@npm:^2.1.2":
version: 2.3.11
resolution: "unplugin@npm:2.3.11"
dependencies:
"@jridgewell/remapping": "npm:^2.3.5"
acorn: "npm:^8.15.0"
picomatch: "npm:^4.0.3"
webpack-virtual-modules: "npm:^0.6.2"
checksum: 10c0/273c1eab0eca4470c7317428689295c31dbe8ab0b306504de9f03cd20c156debb4131bef24b27ac615862958c5dd950a3951d26c0723ea774652ab3624149cff
languageName: node
linkType: hard
"unplugin@npm:^2.3.5":
version: 2.3.10
resolution: "unplugin@npm:2.3.10"
@ -29489,6 +29828,15 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.6.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
languageName: node
linkType: hard
"utf8-byte-length@npm:^1.0.1":
version: 1.0.5
resolution: "utf8-byte-length@npm:1.0.5"
@ -30482,6 +30830,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.24.2":
version: 3.25.76
resolution: "zod@npm:3.25.76"
checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c
languageName: node
linkType: hard
"zod@npm:^3.25.76 || ^4":
version: 4.1.12
resolution: "zod@npm:4.1.12"