修复快速开始新对话以及点击项目无法跳转
This commit is contained in:
24
src/App.tsx
24
src/App.tsx
@@ -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 } }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,14 +278,13 @@ 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 }}
|
|
||||||
exit={{ opacity: 0, y: -10 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className={`h-full w-full ${panelVisibilityClass}`}
|
|
||||||
>
|
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
@@ -311,7 +294,8 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
|
|||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</motion.div>
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -428,7 +412,6 @@ 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}
|
||||||
@@ -436,7 +419,6 @@ export const TabContent: React.FC = () => {
|
|||||||
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">
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user