src/renderer/routes/README.md
This project uses TanStack Router + Multi MemoryRouter architecture, where each Tab has its own independent router instance, enabling native KeepAlive behavior.
Create a file in the src/renderer/routes/ directory:
// routes/knowledge.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/knowledge')({
component: KnowledgePage
})
function KnowledgePage() {
return <div>Knowledge Page</div>
}
After running yarn dev, TanStack Router will automatically update routeTree.gen.ts.
// routes/chat/$topicId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/chat/$topicId')({
component: ChatPage
})
function ChatPage() {
const { topicId } = Route.useParams()
return <div>Chat: {topicId}</div>
}
routes/
├── settings.tsx # /settings (layout)
├── settings/
│ ├── general.tsx # /settings/general
│ └── provider.tsx # /settings/provider
// routes/settings.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/settings')({
component: SettingsLayout
})
function SettingsLayout() {
return (
<div className="flex">
<aside>Settings Menu</aside>
<main><Outlet /></main>
</div>
)
}
This project provides two navigation methods:
openTabOpen a new Tab or switch to an existing Tab using the useTabs hook:
import { useTabs } from '@renderer/hooks/tab'
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)
}
useNavigateNavigate within the same Tab (won't create a new Tab) using TanStack Router's useNavigate:
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' } })
}
| 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 |
useTabs() Return Value| Property/Method | Type | Description |
|---|---|---|
tabs | Tab[] | List of all Tabs |
activeTabId | string | Currently active Tab ID |
activeTab | Tab | undefined | Currently active Tab object |
openTab(url, options?) | (url: string, options?: OpenTabOptions) => string | Open Tab, returns Tab ID |
closeTab(id) | (id: string) => void | Close specified Tab |
setActiveTab(id) | (id: string) => void | Switch to specified Tab |
updateTab(id, updates) | (id: string, updates: Partial<Tab>) => void | Update Tab properties |
OpenTabOptions| Option | Type | Default | Description |
|---|---|---|---|
forceNew | boolean | false | Force create new Tab |
title | string | URL path | Tab title |
type | 'route' | 'webview' | 'route' | Tab type |
id | string | Auto-generated | Custom Tab ID |
AppShell
├── Sidebar
├── TabBar
└── Content Area
├── TabRouter #1 (Home)
│ └── Activity(visible) → MemoryRouter → RouterProvider
├── TabRouter #2 (Settings)
│ └── Activity(hidden) → MemoryRouter → RouterProvider
└── WebviewContainer (for webview tabs)
MemoryRouter instance<Activity> component to control visibility| Layer | Mechanism | Scope |
|---|---|---|
| Route render error | defaultErrorComponent: RouteErrorFallback on every per-tab router (TabRouter.tsx) | Contained to the throwing tab; themed error card with retry/reload |
| Provider render error | Window-level <ErrorBoundary fallbackComponent={WindowFatalFallback}> in each window App | Whole window falls back to a context-free fatal page instead of a white screen |
errorComponent route optiondefaultErrorComponent, TanStack wraps matches in a pass-through fragment: a route render error would bubble to the window-level boundary and tear down the whole windowsrc/renderer/
├── routes/ # Route pages (TanStack Router file-based routing)
│ ├── __root.tsx # Root route (renders Outlet)
│ ├── settings.tsx # /settings
│ ├── settings.index.tsx # /settings/ index route (flat dot form — never a bare index.tsx)
│ └── 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)
routeTree.gen.ts - It is automatically generated by TanStack Routerroutes/settings.tsx → /settings$ - routes/chat/$topicId.tsx → /chat/:topicIduseState, scroll position, etc.