From 72a51fac24b5c092264c18cbaf51f7195aed86f7 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 13 Oct 2025 21:47:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BF=AB=E9=80=9F=E5=BC=80?= =?UTF-8?q?=E5=A7=8B=E6=96=B0=E5=AF=B9=E8=AF=9D=E4=BB=A5=E5=8F=8A=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E9=A1=B9=E7=9B=AE=E6=97=A0=E6=B3=95=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 24 +++++++-- src/components/ClaudeCodeSession.tsx | 33 +++++++++--- src/components/TabContent.tsx | 80 +++++++++++----------------- src/components/TabManager.tsx | 7 ++- src/components/Terminal.tsx | 11 +++- src/contexts/TabContext.tsx | 41 ++++++++++++-- 6 files changed, 130 insertions(+), 66 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e829551..8062918 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -58,7 +58,14 @@ type View = function AppContent() { const { t } = useTranslation(); const [view, setView] = useState("welcome"); - const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState(); + const { + createClaudeMdTab, + createSettingsTab, + createUsageTab, + createMCPTab, + createChatTab, + canAddTab + } = useTabState(); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [sessions, setSessions] = useState([]); @@ -252,9 +259,18 @@ function AppContent() { /** * Opens a new Claude Code session in the interactive UI */ - const handleNewSession = async () => { - handleViewChange("tabs"); - // The tab system will handle creating a new chat tab + const handleNewSession = () => { + if (!canAddTab()) { + return; + } + + const newTabId = createChatTab(); + + if (view !== "tabs") { + setView("tabs"); + } else { + window.dispatchEvent(new CustomEvent('switch-to-tab', { detail: { tabId: newTabId } })); + } }; /** diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 8352ac1..5b813b4 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -32,6 +32,7 @@ import { Popover } from "@/components/ui/popover"; import { useTranslation } from "react-i18next"; import { api, type Session } from "@/lib/api"; import { cn } from "@/lib/utils"; +import { useTabState } from "@/hooks/useTabState"; import { open } from "@tauri-apps/plugin-dialog"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { StreamMessage } from "./StreamMessage"; @@ -77,6 +78,10 @@ interface ClaudeCodeSessionProps { * Initial project path (for new sessions) */ initialProjectPath?: string; + /** + * Tab ID (for syncing state back to tab) + */ + tabId?: string; /** * Callback to go back */ @@ -104,18 +109,20 @@ interface ClaudeCodeSessionProps { export const ClaudeCodeSession: React.FC = ({ session, initialProjectPath = "", + tabId, onBack, onProjectSettings, className, onStreamingChange, }) => { const { t } = useTranslation(); + const { updateTab } = useTabState(); const layoutManager = useLayoutManager(initialProjectPath || session?.project_path); - const { - layout, - breakpoints, - toggleFileExplorer, - toggleGitPanel, + const { + layout, + breakpoints, + toggleFileExplorer, + toggleGitPanel, toggleTimeline, setPanelWidth, setSplitPosition: setLayoutSplitPosition, @@ -128,7 +135,7 @@ export const ClaudeCodeSession: React.FC = ({ closeTerminal, toggleTerminalMaximize } = layoutManager; - + const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || ""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -466,7 +473,19 @@ export const ClaudeCodeSession: React.FC = ({ useEffect(() => { onStreamingChange?.(isLoading, claudeSessionId); }, [isLoading, claudeSessionId, onStreamingChange]); - + + // Sync projectPath to tab when it changes (for persistence) + useEffect(() => { + if (tabId && projectPath && !session) { + // Only update for new sessions (not resumed sessions) + // This ensures the path is saved when user selects/enters it + console.log('[ClaudeCodeSession] Syncing projectPath to tab:', { tabId, projectPath }); + updateTab(tabId, { + initialProjectPath: projectPath + }); + } + }, [projectPath, tabId, session, updateTab]); + // 滚动到顶部 const scrollToTop = useCallback(() => { if (parentRef.current) { diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx index 873007c..20406be 100644 --- a/src/components/TabContent.tsx +++ b/src/components/TabContent.tsx @@ -3,12 +3,11 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useTabState } from '@/hooks/useTabState'; import { useScreenTracking } from '@/hooks/useAnalytics'; import { Tab } from '@/contexts/TabContext'; -import { Loader2, Plus } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; import { api, type Project, type Session, type ClaudeMdFile } from '@/lib/api'; import { ProjectList } from '@/components/ProjectList'; import { SessionList } from '@/components/SessionList'; import { RunningClaudeSessions } from '@/components/RunningClaudeSessions'; -import { Button } from '@/components/ui/button'; import { useTranslation } from '@/hooks/useTranslation'; // Lazy load heavy components @@ -162,36 +161,20 @@ const TabPanel: React.FC = ({ tab, isActive }) => { animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 20 }} transition={{ duration: 0.3 }} + className="space-y-6" > - {/* New session button at the top */} - - - - - {/* Running Claude Sessions */} + {/* Running Claude Sessions - moved before project list */} - {/* Project list */} + {/* Project list - now includes new session button and search */} {projects.length > 0 ? ( { - // Project settings functionality can be added here if needed console.log('Project settings clicked for:', project); }} + onNewSession={handleNewSession} loading={loading} className="animate-fade-in" /> @@ -215,6 +198,7 @@ const TabPanel: React.FC = ({ tab, isActive }) => { { // Go back to projects view in the same tab updateTab(tab.id, { @@ -294,24 +278,24 @@ const TabPanel: React.FC = ({ tab, isActive }) => { } }; + // Only render content when the tab is active or was previously active (to keep state) + // This prevents unnecessary unmounting/remounting + const shouldRenderContent = isActive; + return ( - - - - - } - > - {renderContent()} - - +
+ {shouldRenderContent && ( + + +
+ } + > + {renderContent()} + + )} + ); }; @@ -319,7 +303,7 @@ export const TabContent: React.FC = () => { const { t } = useTranslation(); const { tabs, activeTabId, createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab } = useTabState(); const [hasInitialized, setHasInitialized] = React.useState(false); - + // Debug: Monitor activeTabId changes useEffect(() => { }, [activeTabId, tabs]); @@ -428,15 +412,13 @@ export const TabContent: React.FC = () => { return (
- - {tabs.map((tab) => ( - - ))} - + {tabs.map((tab) => ( + + ))} {tabs.length === 0 && (
diff --git a/src/components/TabManager.tsx b/src/components/TabManager.tsx index c8e467e..073cc92 100644 --- a/src/components/TabManager.tsx +++ b/src/components/TabManager.tsx @@ -176,6 +176,9 @@ export const TabManager: React.FC = ({ className }) => { // Listen for keyboard shortcut events useEffect(() => { const handleCreateTab = () => { + if (!canAddTab()) { + return; + } createChatTab(); trackEvent.tabCreated('chat'); }; @@ -246,7 +249,7 @@ export const TabManager: React.FC = ({ className }) => { window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener); window.removeEventListener('open-session-tab', handleOpenSessionTab as EventListener); }; - }, [tabs, activeTabId, createChatTab, closeTab, switchToTab, updateTab, canAddTab]); + }, [tabs, activeTabId, createChatTab, closeTab, switchToTab, updateTab, canAddTab, trackEvent]); // Check scroll buttons visibility const checkScrollButtons = () => { @@ -427,4 +430,4 @@ export const TabManager: React.FC = ({ className }) => { ); }; -export default TabManager; \ No newline at end of file +export default TabManager; diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx index b8dcea6..f21bb95 100644 --- a/src/components/Terminal.tsx +++ b/src/components/Terminal.tsx @@ -277,8 +277,17 @@ export const Terminal: React.FC = ({ resizeTerminal(); }, 150); + // 如果没有有效的 projectPath,跳过创建终端会话 + if (!projectPath || projectPath.trim() === '') { + console.log('[Terminal] Skipping session creation - no project path'); + if (xtermRef.current) { + xtermRef.current.write('\r\n\x1b[33mNo project directory selected. Please select a project to use the terminal.\x1b[0m\r\n'); + } + return; + } + // 创建终端会话 - const newSessionId = await api.createTerminalSession(projectPath || process.cwd()); + const newSessionId = await api.createTerminalSession(projectPath); if (!isMounted) { await api.closeTerminalSession(newSessionId); diff --git a/src/contexts/TabContext.tsx b/src/contexts/TabContext.tsx index 86eb1ef..78dfcb9 100644 --- a/src/contexts/TabContext.tsx +++ b/src/contexts/TabContext.tsx @@ -43,9 +43,12 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children const [activeTabId, setActiveTabId] = useState(null); - // Always start with a fresh CC Projects tab + // Ensure there is always at least one projects tab, but avoid clobbering existing tabs useEffect(() => { - // Create default projects tab + if (tabs.length > 0) { + return; + } + const defaultTab: Tab = { id: generateTabId(), type: 'projects', @@ -53,12 +56,44 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children status: 'idle', hasUnsavedChanges: false, order: 0, + icon: 'folder', createdAt: new Date(), updatedAt: new Date() }; + setTabs([defaultTab]); setActiveTabId(defaultTab.id); - }, [t]); + }, [tabs.length, t]); + + // Keep the projects tab title in sync with the active locale without resetting other tabs + useEffect(() => { + if (tabs.length === 0) { + return; + } + + const translatedTitle = t('ccProjects'); + setTabs(prevTabs => { + let hasChanges = false; + const updatedTabs = prevTabs.map(tab => { + if (tab.type === 'projects' && tab.title !== translatedTitle) { + hasChanges = true; + return { + ...tab, + title: translatedTitle + }; + } + return tab; + }); + return hasChanges ? updatedTabs : prevTabs; + }); + }, [t, tabs.length]); + + // Guard against an empty activeTabId when tabs exist + useEffect(() => { + if (!activeTabId && tabs.length > 0) { + setActiveTabId(tabs[0].id); + } + }, [activeTabId, tabs]); // Tab persistence disabled - no longer saving to localStorage // useEffect(() => {