From 962ee5d2700dc901f87e23498403ce17a309bc41 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Thu, 4 Dec 2025 13:55:04 +0800 Subject: [PATCH] refactor: update architecture documentation and implement MemoryRouter enhancements - Changed code snippets in the architecture documentation to use the `text` syntax for better clarity. - Replaced CSS `display:none` with React 19.2 `` component for improved effect management and memory optimization. - Documented the design decision to use a Pure MemoryRouter over HashRouter for detached windows, emphasizing simplicity and consistency. - Enhanced the Tab Detach implementation with detailed explanations of state passing via URL query and IPC, along with architecture flow diagrams. - Added new components and functions to support detached tab functionality, including state restoration and memory management strategies. These updates improve the documentation and functionality of the tab management system, ensuring better performance and user experience. --- .../architecture-multi-memory-router.md | 301 +++++++++++++++--- 1 file changed, 260 insertions(+), 41 deletions(-) diff --git a/src/renderer/src/components/layout/architecture-multi-memory-router.md b/src/renderer/src/components/layout/architecture-multi-memory-router.md index d9af072262..efb7c8bc3b 100644 --- a/src/renderer/src/components/layout/architecture-multi-memory-router.md +++ b/src/renderer/src/components/layout/architecture-multi-memory-router.md @@ -9,7 +9,7 @@ ### Core Contradiction -``` +```text URL Router Design Philosophy: URL change → Component switch (single active view) Tab System Requirement: Multiple views coexist, only switch visibility (preserve state) ``` @@ -48,7 +48,7 @@ These two are fundamentally conflicting. The current architecture uses TanStack VS Code 1.85 implemented "Auxiliary Window" feature: -``` +```text ┌─────────────────────────────────────────────────────────┐ │ Main Window │ │ ┌─────────────────────────────────────────────────────┐│ @@ -134,7 +134,7 @@ If not using ``, TSR feature availability: Each Tab has an independent MemoryRouter instance, achieving state isolation and KeepAlive. -``` +```text ┌─────────────────────────────────────────────────────────┐ │ AppShell │ │ ┌─────────────────────────────────────────────────────┐│ @@ -193,9 +193,10 @@ export interface TabsState { ```typescript // src/renderer/src/components/layout/TabRouter.tsx +import { Activity } from 'react' // React 19.2+ import { createRouter, RouterProvider } from '@tanstack/react-router' import { createMemoryHistory } from '@tanstack/react-router' -import { useMemo, useEffect, useRef } from 'react' +import { useMemo, useEffect } from 'react' import { routeTree } from '../../routeTree.gen' interface TabRouterProps { @@ -228,20 +229,29 @@ export const TabRouter = ({ tab, isActive, onUrlChange }: TabRouterProps) => { return unsubscribe }, [router, tab.url, onUrlChange]) + // Use React 19.2 Activity for visibility control + // Benefits over CSS display:none: + // - Effects unmount when hidden (timers, subscriptions cleaned up) + // - Effects re-mount when visible (fresh state) + // - Better React integration and memory optimization return ( -
+ -
+
) } ``` +> **Why Activity over CSS `display:none`?** +> +> | Aspect | CSS `display:none` | React `` | +> |--------|-------------------|-------------------| +> | DOM preserved | ✅ | ✅ | +> | State preserved | ✅ | ✅ | +> | Effects (timers, subscriptions) | ❌ Keep running | ✅ **Cleanup when hidden** | +> | Memory optimization | ❌ None | ✅ React can optimize | +> | Suspense integration | ❌ None | ✅ Better boundaries | + #### 4.3.3 AppShell Component ```typescript @@ -320,48 +330,254 @@ export const AppShell = () => { ### 4.4 Tab Detach to Window -#### 4.4.1 State Serialization +#### 4.4.1 Design Decision: Pure MemoryRouter (No HashRouter Needed) + +**Question**: Should we use HashRouter for detached windows so the URL can specify the route? + +```text +Option A: HashRouter + index.html#/chat/123 + └── HashRouter reads hash → route matches → loader runs + +Option B: Pure MemoryRouter (Recommended) + index.html?path=/chat/123 + └── Read URL query → MemoryRouter initializes → loader runs +``` + +**Analysis**: + +| Aspect | HashRouter | Pure MemoryRouter | +|--------|------------|-------------------| +| Loader execution | ✅ Works | ✅ Works | +| Route matching | ✅ Works | ✅ Works | +| Code consistency | Two router types | **Unified** | +| State passing | Hash only | **URL query (flexible)** | + +**Conclusion**: Pure MemoryRouter is simpler. TSR loader runs when MemoryRouter initializes with `initialEntries`, no HashRouter needed. + +#### 4.4.2 State Passing: URL Query vs IPC + +```text +┌─────────────────────────────────────────────────────────┐ +│ IPC Approach: │ +│ 1. Renderer → IPC → Main process │ +│ 2. Main process → create window → load index.html │ +│ 3. Main process → IPC → New renderer │ +│ 4. New renderer receives state │ +│ │ +│ URL Query Approach (Recommended): │ +│ 1. Renderer → Main process │ +│ 2. Main process → create window │ +│ 3. load index.html?path=/chat/123&scroll=500 │ +│ 4. New renderer reads window.location.search ✨ │ +└─────────────────────────────────────────────────────────┘ +``` + +| Aspect | URL Query | IPC | +|--------|-----------|-----| +| Complexity | **Simple** | Requires bidirectional communication | +| Data size | URL length limit (~2KB) | Unlimited | +| Data types | Strings only | Any serializable | +| Security | Visible in URL | Process internal | + +**Recommendation**: Use URL query for simple state (path, scroll), use shared cache for complex state (long inputDraft). + +#### 4.4.3 Architecture Flow + +```text +┌─────────────────────────────────────────────────────────┐ +│ Main Window - Tab Detach │ +│ │ +│ Tab (MemoryRouter) │ +│ router.state.location.pathname = '/chat/123' │ +│ (Browser URL unchanged, path is in memory) │ +│ │ │ +│ │ Drag out │ +│ ▼ │ +│ Construct URL: index.html?path=/chat/123&scroll=500 │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐│ +│ │ New Window ││ +│ │ ││ +│ │ const params = new URLSearchParams(location.search) ││ +│ │ const path = params.get('path') // '/chat/123' ││ +│ │ ││ +│ │ createMemoryHistory({ initialEntries: [path] }) ││ +│ │ │ ││ +│ │ ▼ ││ +│ │ TSR matches route → loader() executes → render ││ +│ └─────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────┘ +``` + +#### 4.4.4 Implementation + +**Router Factory**: ```typescript -// On detach: serialize current state -const serializeTabState = (tab: Tab): SerializedTabState => { - return { - ...tab, - scrollPosition: captureScrollPosition(tab.id), - inputDraft: captureInputDraft(tab.id), +// src/renderer/src/lib/createTabRouter.ts +import { createRouter, createMemoryHistory } from '@tanstack/react-router' +import { routeTree } from '../routeTree.gen' + +export function createTabRouter(initialPath: string = '/') { + const history = createMemoryHistory({ + initialEntries: [initialPath] + }) + return createRouter({ routeTree, history }) +} +``` + +**Main Window - Detach Handler**: + +```typescript +// src/renderer/src/components/layout/AppShell.tsx +const handleDetachTab = async (tab: Tab) => { + const path = tab.router.state.location.pathname + const search = tab.router.state.location.search + const scroll = captureScrollPosition(tab.id) + + // Construct URL with query params + const params = new URLSearchParams({ + path: path + search, + scroll: String(scroll) + }) + + // For complex state (like inputDraft), save to shared cache + if (tab.inputDraft) { + await cacheService.set(`detached:${tab.id}:draft`, tab.inputDraft) + params.set('tabId', tab.id) } -} -// On new window startup: deserialize to restore state -const deserializeTabState = (state: SerializedTabState): Tab => { - // MemoryRouter will initialize from state.url - // Scroll position, etc. restored after component mounts - return state + // Request main process to create window with URL + await window.api.createWindow({ + url: `index.html?${params.toString()}` + }) + + closeTab(tab.id) } ``` -#### 4.4.2 IPC Communication +**App Entry - Detect Detached Window**: ```typescript -// Main Process -ipcMain.handle('create-window', async (_, options) => { - const newWindow = new BrowserWindow({ - // ... - }) +// src/renderer/src/App.tsx +const App = () => { + const params = new URLSearchParams(window.location.search) + const initialPath = params.get('path') + const initialScroll = Number(params.get('scroll')) || 0 + const tabId = params.get('tabId') - // Pass initial state to new window - newWindow.webContents.once('did-finish-load', () => { - newWindow.webContents.send('init-tab', options.initialTab) - }) -}) + // If has path param, this is a detached window + if (initialPath) { + return ( + + ) + } -// Renderer Process (new window) -window.api.onInitTab((serializedTab) => { - const tab = deserializeTabState(JSON.parse(serializedTab)) - tabStore.addTab(tab) -}) + // Main window with full tab system + return +} ``` +**Detached Window Component**: + +```typescript +// src/renderer/src/components/DetachedTabWindow.tsx +interface Props { + path: string + scroll: number + tabId?: string | null +} + +const DetachedTabWindow = ({ path, scroll, tabId }: Props) => { + const router = useMemo(() => createTabRouter(path), [path]) + const [inputDraft, setInputDraft] = useState() + + // Restore scroll position after mount + useEffect(() => { + if (scroll) { + requestAnimationFrame(() => window.scrollTo(0, scroll)) + } + }, [scroll]) + + // Restore complex state from shared cache + useEffect(() => { + if (tabId) { + cacheService.get(`detached:${tabId}:draft`).then(setInputDraft) + } + }, [tabId]) + + // TSR automatically: match route → run loader → render + return +} +``` + +#### 4.4.5 TSR Loader Works with MemoryRouter + +Key insight: TSR's `loader` runs during route matching, regardless of history type. + +```typescript +// src/renderer/src/routes/chat/$topicId.tsx +export const Route = createFileRoute('/chat/$topicId')({ + loader: async ({ params }) => { + // Runs for BOTH HashRouter and MemoryRouter + const topic = await fetchTopic(params.topicId) + const messages = await fetchMessages(params.topicId) + return { topic, messages } + }, + component: ChatPage +}) + +function ChatPage() { + const { topic, messages } = Route.useLoaderData() + return +} +``` + +When `createMemoryHistory({ initialEntries: ['/chat/123'] })` is called: + +1. TSR parses path `/chat/123` +2. Matches route `/chat/$topicId` +3. Extracts params: `{ topicId: '123' }` +4. Executes `loader({ params })` +5. Renders component with loaded data + +#### 4.4.6 Tab Attach (Drag Back to Main Window) + +```text +┌─────────────────────────────────────────────────────────┐ +│ Tab Lifecycle │ +│ │ +│ Main Window Detached Window │ +│ ┌─────────┐ ┌─────────┐ │ +│ │ Tab 1 │ ──── Drag Out ────► │ Window │ │ +│ │ Tab 2 │ (URL Query) │ │ │ +│ │ Tab 3 │ ◄─── Drag Back ──── │ │ │ +│ └─────────┘ (IPC) └─────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +Drag Out vs Drag Back: + +| Aspect | Drag Out | Drag Back | +|--------|----------|-----------| +| State passing | URL Query | IPC (reverse direction) | +| Detection | Simple (user action) | Detect drop on main window tab bar | +| Window action | Create new window | Close detached window | +| Complexity | Low | Medium | + +**Implementation Key Points**: + +1. **Drop Detection**: Detached window monitors drag events, checks if drop position overlaps main window's tab bar bounds +2. **State Transfer**: Use IPC to send current state (path, scroll, inputDraft) back to main window +3. **Window Coordination**: Main process coordinates between windows - add tab to main, close detached +4. **Graceful Fallback**: If drop detection fails, provide "Attach to Main Window" button as alternative + ### 4.5 Memory Management #### 4.5.1 LRU Eviction Strategy @@ -471,4 +687,7 @@ Reasons for choosing **MemoryHistory Multi-Instance**: | Version | Date | Changes | |---------|------|---------| +| v1.3.0 | 2025-12-04 | Added Tab Attach (drag back) key points | +| v1.2.0 | 2025-12-04 | Replaced CSS `display:none` with React 19.2 `` for better effect management | +| v1.1.0 | 2025-12-03 | Added detailed Tab Detach implementation (URL Query approach, TSR Loader compatibility) | | v1.0.0 | 2025-12-03 | Initial research report |