diff --git a/packages/ui/src/components/FileExplorer.tsx b/packages/ui/src/components/FileExplorer.tsx index 6e91912..425d537 100644 --- a/packages/ui/src/components/FileExplorer.tsx +++ b/packages/ui/src/components/FileExplorer.tsx @@ -132,11 +132,21 @@ function TreeNode({ node, depth, onFileSelect, expandedPaths, onToggleExpand }: ); } +const STORAGE_KEY_EXPANDED = 'ai-assistant-file-explorer-expanded'; + export function FileExplorer({ onFileSelect, className, workingDirectory }: FileExplorerProps) { const [tree, setTree] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [expandedPaths, setExpandedPaths] = useState>(new Set()); + // 从 localStorage 恢复展开状态 + const [expandedPaths, setExpandedPaths] = useState>(() => { + try { + const saved = localStorage.getItem(STORAGE_KEY_EXPANDED); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } catch { + return new Set(); + } + }); // 加载文件树 const loadTree = useCallback(async () => { @@ -146,13 +156,7 @@ export function FileExplorer({ onFileSelect, className, workingDirectory }: File const result = await getFileTree('.', 4); if (result.success) { setTree(result.data.tree); - // 默认展开第一层 - const firstLevel = new Set( - result.data.tree - .filter((n) => n.type === 'directory') - .map((n) => n.path) - ); - setExpandedPaths(firstLevel); + // 保持已保存的展开状态(不重置) } else { setError('Failed to load file tree'); } @@ -175,6 +179,8 @@ export function FileExplorer({ onFileSelect, className, workingDirectory }: File } else { next.add(path); } + // 保存到 localStorage + localStorage.setItem(STORAGE_KEY_EXPANDED, JSON.stringify([...next])); return next; }); }, []); diff --git a/packages/ui/src/components/IDE.tsx b/packages/ui/src/components/IDE.tsx index b421efe..9bba172 100644 --- a/packages/ui/src/components/IDE.tsx +++ b/packages/ui/src/components/IDE.tsx @@ -4,13 +4,23 @@ * 整合文件浏览器和代码编辑器的 IDE 组件 */ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { toast } from 'sonner'; import { cn } from '../utils/cn.js'; import { readFile, getWorkingDirectory } from '../api/client.js'; import { FileExplorer } from './FileExplorer.js'; import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js'; +// localStorage 键名 +const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs'; +const STORAGE_KEY_ACTIVE_TAB = 'ai-assistant-editor-active-tab'; + +// 保存的标签页格式(只保存路径,不保存内容) +interface SavedTab { + path: string; + name: string; +} + interface IDEProps { className?: string; /** 文件浏览器宽度 */ @@ -21,6 +31,8 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) { const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); const [workingDirectory, setWorkingDirectory] = useState(''); + const isRestoringTabs = useRef(false); + const hasRestoredTabs = useRef(false); // 获取工作目录 useEffect(() => { @@ -35,6 +47,93 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) { }); }, []); + // 从 localStorage 恢复标签页 + useEffect(() => { + if (hasRestoredTabs.current) return; + hasRestoredTabs.current = true; + + const restoreTabs = async () => { + try { + const savedTabsJson = localStorage.getItem(STORAGE_KEY_TABS); + const savedActivePath = localStorage.getItem(STORAGE_KEY_ACTIVE_TAB); + + if (!savedTabsJson) return; + + const savedTabs: SavedTab[] = JSON.parse(savedTabsJson); + if (savedTabs.length === 0) return; + + isRestoringTabs.current = true; + + // 逐个恢复标签页 + const restoredTabs: EditorTab[] = []; + for (const savedTab of savedTabs) { + try { + const result = await readFile(savedTab.path); + if (result.success && result.data.encoding === 'utf-8') { + restoredTabs.push({ + id: `tab-${Date.now()}-${restoredTabs.length}`, + path: savedTab.path, + name: savedTab.name, + content: result.data.content, + originalContent: result.data.content, + language: getLanguageFromFilename(savedTab.name), + }); + } + } catch { + // 文件可能已删除,跳过 + } + } + + if (restoredTabs.length > 0) { + setTabs(restoredTabs); + + // 恢复活动标签 + if (savedActivePath) { + const activeTab = restoredTabs.find((tab) => tab.path === savedActivePath); + if (activeTab) { + setActiveTabId(activeTab.id); + } else { + setActiveTabId(restoredTabs[0].id); + } + } else { + setActiveTabId(restoredTabs[0].id); + } + } + + isRestoringTabs.current = false; + } catch { + isRestoringTabs.current = false; + } + }; + + restoreTabs(); + }, []); + + // 保存标签页到 localStorage + useEffect(() => { + // 恢复过程中不保存 + if (isRestoringTabs.current) return; + + const tabsToSave: SavedTab[] = tabs.map((tab) => ({ + path: tab.path, + name: tab.name, + })); + localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(tabsToSave)); + }, [tabs]); + + // 保存活动标签到 localStorage + useEffect(() => { + // 恢复过程中不保存 + if (isRestoringTabs.current) return; + + const activeTab = tabs.find((tab) => tab.id === activeTabId); + if (activeTab) { + localStorage.setItem(STORAGE_KEY_ACTIVE_TAB, activeTab.path); + } else { + localStorage.removeItem(STORAGE_KEY_ACTIVE_TAB); + } + }, [activeTabId, tabs]); + // 打开文件 const handleFileSelect = useCallback(async (path: string, name: string) => { // 检查是否已打开 diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 38c59d3..9a572a4 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -42,7 +42,7 @@ export function App() { // IDE 面板宽度(百分比) const [idePanelWidth, setIdePanelWidth] = useState(() => { const saved = localStorage.getItem('ai-assistant-ide-width'); - return saved ? parseFloat(saved) : 60; + return saved ? parseFloat(saved) : 70; }); // 初始化:加载会话