修复快速开始新对话以及点击项目无法跳转

This commit is contained in:
2025-10-13 21:47:52 +08:00
parent 7d3941780f
commit 72a51fac24
6 changed files with 130 additions and 66 deletions

View File

@@ -58,7 +58,14 @@ type View =
function AppContent() { function AppContent() {
const { t } = useTranslation(); const { t } = useTranslation();
const [view, setView] = useState<View>("welcome"); const [view, setView] = useState<View>("welcome");
const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState(); const {
createClaudeMdTab,
createSettingsTab,
createUsageTab,
createMCPTab,
createChatTab,
canAddTab
} = useTabState();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState<Project | null>(null); const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [sessions, setSessions] = useState<Session[]>([]); const [sessions, setSessions] = useState<Session[]>([]);
@@ -252,9 +259,18 @@ function AppContent() {
/** /**
* Opens a new Claude Code session in the interactive UI * Opens a new Claude Code session in the interactive UI
*/ */
const handleNewSession = async () => { const handleNewSession = () => {
handleViewChange("tabs"); if (!canAddTab()) {
// The tab system will handle creating a new chat tab return;
}
const newTabId = createChatTab();
if (view !== "tabs") {
setView("tabs");
} else {
window.dispatchEvent(new CustomEvent('switch-to-tab', { detail: { tabId: newTabId } }));
}
}; };
/** /**

View File

@@ -32,6 +32,7 @@ import { Popover } from "@/components/ui/popover";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { api, type Session } from "@/lib/api"; import { api, type Session } from "@/lib/api";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTabState } from "@/hooks/useTabState";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { StreamMessage } from "./StreamMessage"; import { StreamMessage } from "./StreamMessage";
@@ -77,6 +78,10 @@ interface ClaudeCodeSessionProps {
* Initial project path (for new sessions) * Initial project path (for new sessions)
*/ */
initialProjectPath?: string; initialProjectPath?: string;
/**
* Tab ID (for syncing state back to tab)
*/
tabId?: string;
/** /**
* Callback to go back * Callback to go back
*/ */
@@ -104,12 +109,14 @@ interface ClaudeCodeSessionProps {
export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
session, session,
initialProjectPath = "", initialProjectPath = "",
tabId,
onBack, onBack,
onProjectSettings, onProjectSettings,
className, className,
onStreamingChange, onStreamingChange,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { updateTab } = useTabState();
const layoutManager = useLayoutManager(initialProjectPath || session?.project_path); const layoutManager = useLayoutManager(initialProjectPath || session?.project_path);
const { const {
layout, layout,
@@ -467,6 +474,18 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
onStreamingChange?.(isLoading, claudeSessionId); onStreamingChange?.(isLoading, claudeSessionId);
}, [isLoading, claudeSessionId, onStreamingChange]); }, [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(() => { const scrollToTop = useCallback(() => {
if (parentRef.current) { if (parentRef.current) {

View File

@@ -3,12 +3,11 @@ import { motion, AnimatePresence } from 'framer-motion';
import { useTabState } from '@/hooks/useTabState'; import { useTabState } from '@/hooks/useTabState';
import { useScreenTracking } from '@/hooks/useAnalytics'; import { useScreenTracking } from '@/hooks/useAnalytics';
import { Tab } from '@/contexts/TabContext'; 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 { api, type Project, type Session, type ClaudeMdFile } from '@/lib/api';
import { ProjectList } from '@/components/ProjectList'; import { ProjectList } from '@/components/ProjectList';
import { SessionList } from '@/components/SessionList'; import { SessionList } from '@/components/SessionList';
import { RunningClaudeSessions } from '@/components/RunningClaudeSessions'; import { RunningClaudeSessions } from '@/components/RunningClaudeSessions';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/useTranslation'; import { useTranslation } from '@/hooks/useTranslation';
// Lazy load heavy components // Lazy load heavy components
@@ -162,36 +161,20 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }} exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="space-y-6"
> >
{/* New session button at the top */} {/* Running Claude Sessions - moved before project list */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-4"
>
<Button
onClick={handleNewSession}
size="default"
className="w-full max-w-md"
>
<Plus className="mr-2 h-4 w-4" />
{t('newClaudeCodeSession')}
</Button>
</motion.div>
{/* Running Claude Sessions */}
<RunningClaudeSessions /> <RunningClaudeSessions />
{/* Project list */} {/* Project list - now includes new session button and search */}
{projects.length > 0 ? ( {projects.length > 0 ? (
<ProjectList <ProjectList
projects={projects} projects={projects}
onProjectClick={handleProjectClick} onProjectClick={handleProjectClick}
onProjectSettings={(project) => { onProjectSettings={(project) => {
// Project settings functionality can be added here if needed
console.log('Project settings clicked for:', project); console.log('Project settings clicked for:', project);
}} }}
onNewSession={handleNewSession}
loading={loading} loading={loading}
className="animate-fade-in" className="animate-fade-in"
/> />
@@ -215,6 +198,7 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
<ClaudeCodeSession <ClaudeCodeSession
session={tab.sessionData} // Pass the full session object if available session={tab.sessionData} // Pass the full session object if available
initialProjectPath={tab.initialProjectPath || tab.sessionId} initialProjectPath={tab.initialProjectPath || tab.sessionId}
tabId={tab.id} // Pass tabId for state synchronization
onBack={() => { onBack={() => {
// Go back to projects view in the same tab // Go back to projects view in the same tab
updateTab(tab.id, { updateTab(tab.id, {
@@ -294,24 +278,24 @@ const TabPanel: React.FC<TabPanelProps> = ({ 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 ( return (
<motion.div <div className={`h-full w-full ${panelVisibilityClass}`}>
initial={{ opacity: 0, y: 10 }} {shouldRenderContent && (
animate={{ opacity: 1, y: 0 }} <Suspense
exit={{ opacity: 0, y: -10 }} fallback={
transition={{ duration: 0.2 }} <div className="flex items-center justify-center h-full">
className={`h-full w-full ${panelVisibilityClass}`} <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
> </div>
<Suspense }
fallback={ >
<div className="flex items-center justify-center h-full"> {renderContent()}
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /> </Suspense>
</div> )}
} </div>
>
{renderContent()}
</Suspense>
</motion.div>
); );
}; };
@@ -428,15 +412,13 @@ export const TabContent: React.FC = () => {
return ( return (
<div className="flex-1 h-full relative"> <div className="flex-1 h-full relative">
<AnimatePresence mode="wait"> {tabs.map((tab) => (
{tabs.map((tab) => ( <TabPanel
<TabPanel key={tab.id}
key={tab.id} tab={tab}
tab={tab} isActive={tab.id === activeTabId}
isActive={tab.id === activeTabId} />
/> ))}
))}
</AnimatePresence>
{tabs.length === 0 && ( {tabs.length === 0 && (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">

View File

@@ -176,6 +176,9 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
// Listen for keyboard shortcut events // Listen for keyboard shortcut events
useEffect(() => { useEffect(() => {
const handleCreateTab = () => { const handleCreateTab = () => {
if (!canAddTab()) {
return;
}
createChatTab(); createChatTab();
trackEvent.tabCreated('chat'); trackEvent.tabCreated('chat');
}; };
@@ -246,7 +249,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener); window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
window.removeEventListener('open-session-tab', handleOpenSessionTab 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 // Check scroll buttons visibility
const checkScrollButtons = () => { const checkScrollButtons = () => {

View File

@@ -277,8 +277,17 @@ export const Terminal: React.FC<TerminalProps> = ({
resizeTerminal(); resizeTerminal();
}, 150); }, 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) { if (!isMounted) {
await api.closeTerminalSession(newSessionId); await api.closeTerminalSession(newSessionId);

View File

@@ -43,9 +43,12 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [activeTabId, setActiveTabId] = useState<string | null>(null); const [activeTabId, setActiveTabId] = useState<string | null>(null);
// Always start with a fresh CC Projects tab // Ensure there is always at least one projects tab, but avoid clobbering existing tabs
useEffect(() => { useEffect(() => {
// Create default projects tab if (tabs.length > 0) {
return;
}
const defaultTab: Tab = { const defaultTab: Tab = {
id: generateTabId(), id: generateTabId(),
type: 'projects', type: 'projects',
@@ -53,12 +56,44 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children
status: 'idle', status: 'idle',
hasUnsavedChanges: false, hasUnsavedChanges: false,
order: 0, order: 0,
icon: 'folder',
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
}; };
setTabs([defaultTab]); setTabs([defaultTab]);
setActiveTabId(defaultTab.id); 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 // Tab persistence disabled - no longer saving to localStorage
// useEffect(() => { // useEffect(() => {