feat: implement comprehensive tabbed interface system

- Add TabContext and useTabState for centralized tab management
- Create TabManager component with drag-and-drop reordering
- Implement TabContent component for dynamic content rendering
- Add AgentsModal for enhanced agent management interface
- Integrate tab system into main App component
- Update existing components to work with new tab architecture
- Support multiple tab types: chat, agent, projects, usage, mcp, settings
- Add tab status tracking and unsaved changes detection
- Implement smooth animations and modern UI interactions
This commit is contained in:
Vivek R
2025-07-15 14:08:48 +05:30
parent cee71343f5
commit 9887b9d14a
9 changed files with 1952 additions and 187 deletions

347
src/hooks/useTabState.ts Normal file
View File

@@ -0,0 +1,347 @@
import { useCallback, useMemo } from 'react';
import { useTabContext } from '@/contexts/TabContext';
import { Tab } from '@/contexts/TabContext';
interface UseTabStateReturn {
// State
tabs: Tab[];
activeTab: Tab | undefined;
activeTabId: string | null;
tabCount: number;
chatTabCount: number;
agentTabCount: number;
// Operations
createChatTab: (projectId?: string, title?: string) => string;
createAgentTab: (agentRunId: string, agentName: string) => string;
createAgentExecutionTab: (agent: any, tabId: string) => string;
createProjectsTab: () => string | null;
createUsageTab: () => string | null;
createMCPTab: () => string | null;
createSettingsTab: () => string | null;
createClaudeMdTab: () => string | null;
createClaudeFileTab: (fileId: string, fileName: string) => string;
createCreateAgentTab: () => string;
createImportAgentTab: () => string;
closeTab: (id: string, force?: boolean) => Promise<boolean>;
closeCurrentTab: () => Promise<boolean>;
switchToTab: (id: string) => void;
switchToNextTab: () => void;
switchToPreviousTab: () => void;
switchToTabByIndex: (index: number) => void;
updateTab: (id: string, updates: Partial<Tab>) => void;
updateTabTitle: (id: string, title: string) => void;
updateTabStatus: (id: string, status: Tab['status']) => void;
markTabAsChanged: (id: string, hasChanges: boolean) => void;
findTabBySessionId: (sessionId: string) => Tab | undefined;
findTabByAgentRunId: (agentRunId: string) => Tab | undefined;
findTabByType: (type: Tab['type']) => Tab | undefined;
canAddTab: () => boolean;
}
export const useTabState = (): UseTabStateReturn => {
const {
tabs,
activeTabId,
addTab,
removeTab,
updateTab,
setActiveTab,
getTabById,
getTabsByType
} = useTabContext();
const activeTab = useMemo(() =>
activeTabId ? getTabById(activeTabId) : undefined,
[activeTabId, getTabById]
);
const tabCount = tabs.length;
const chatTabCount = useMemo(() => getTabsByType('chat').length, [getTabsByType]);
const agentTabCount = useMemo(() => getTabsByType('agent').length, [getTabsByType]);
const createChatTab = useCallback((projectId?: string, title?: string): string => {
const tabTitle = title || `Chat ${chatTabCount + 1}`;
return addTab({
type: 'chat',
title: tabTitle,
sessionId: projectId,
status: 'idle',
hasUnsavedChanges: false,
icon: 'message-square'
});
}, [addTab, chatTabCount]);
const createAgentTab = useCallback((agentRunId: string, agentName: string): string => {
// Check if tab already exists
const existingTab = tabs.find(tab => tab.agentRunId === agentRunId);
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'agent',
title: agentName,
agentRunId,
status: 'running',
hasUnsavedChanges: false,
icon: 'bot'
});
}, [addTab, tabs, setActiveTab]);
const createProjectsTab = useCallback((): string | null => {
// Check if projects tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'projects');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'projects',
title: 'CC Projects',
status: 'idle',
hasUnsavedChanges: false,
icon: 'folder'
});
}, [addTab, tabs, setActiveTab]);
const createUsageTab = useCallback((): string | null => {
// Check if usage tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'usage');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'usage',
title: 'Usage',
status: 'idle',
hasUnsavedChanges: false,
icon: 'bar-chart'
});
}, [addTab, tabs, setActiveTab]);
const createMCPTab = useCallback((): string | null => {
// Check if MCP tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'mcp');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'mcp',
title: 'MCP Servers',
status: 'idle',
hasUnsavedChanges: false,
icon: 'server'
});
}, [addTab, tabs, setActiveTab]);
const createSettingsTab = useCallback((): string | null => {
// Check if settings tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'settings');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'settings',
title: 'Settings',
status: 'idle',
hasUnsavedChanges: false,
icon: 'settings'
});
}, [addTab, tabs, setActiveTab]);
const createClaudeMdTab = useCallback((): string | null => {
// Check if claude-md tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'claude-md');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'claude-md',
title: 'CLAUDE.md',
status: 'idle',
hasUnsavedChanges: false,
icon: 'file-text'
});
}, [addTab, tabs, setActiveTab]);
const createClaudeFileTab = useCallback((fileId: string, fileName: string): string => {
// Check if tab already exists for this file
const existingTab = tabs.find(tab => tab.type === 'claude-file' && tab.claudeFileId === fileId);
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'claude-file',
title: fileName,
claudeFileId: fileId,
status: 'idle',
hasUnsavedChanges: false,
icon: 'file-text'
});
}, [addTab, tabs, setActiveTab]);
const createAgentExecutionTab = useCallback((agent: any, _tabId: string): string => {
return addTab({
type: 'agent-execution',
title: `Run: ${agent.name}`,
agentData: agent,
status: 'idle',
hasUnsavedChanges: false,
icon: 'bot'
});
}, [addTab]);
const createCreateAgentTab = useCallback((): string => {
// Check if create agent tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'create-agent');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'create-agent',
title: 'Create Agent',
status: 'idle',
hasUnsavedChanges: false,
icon: 'plus'
});
}, [addTab, tabs, setActiveTab]);
const createImportAgentTab = useCallback((): string => {
// Check if import agent tab already exists (singleton)
const existingTab = tabs.find(tab => tab.type === 'import-agent');
if (existingTab) {
setActiveTab(existingTab.id);
return existingTab.id;
}
return addTab({
type: 'import-agent',
title: 'Import Agent',
status: 'idle',
hasUnsavedChanges: false,
icon: 'import'
});
}, [addTab, tabs, setActiveTab]);
const closeTab = useCallback(async (id: string, force: boolean = false): Promise<boolean> => {
const tab = getTabById(id);
if (!tab) return true;
// Check for unsaved changes
if (!force && tab.hasUnsavedChanges) {
// In a real implementation, you'd show a confirmation dialog here
const confirmed = window.confirm(`Tab "${tab.title}" has unsaved changes. Close anyway?`);
if (!confirmed) return false;
}
removeTab(id);
return true;
}, [getTabById, removeTab]);
const closeCurrentTab = useCallback(async (): Promise<boolean> => {
if (!activeTabId) return true;
return closeTab(activeTabId);
}, [activeTabId, closeTab]);
const switchToNextTab = useCallback(() => {
if (tabs.length === 0) return;
const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);
const nextIndex = (currentIndex + 1) % tabs.length;
setActiveTab(tabs[nextIndex].id);
}, [tabs, activeTabId, setActiveTab]);
const switchToPreviousTab = useCallback(() => {
if (tabs.length === 0) return;
const currentIndex = tabs.findIndex(tab => tab.id === activeTabId);
const previousIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;
setActiveTab(tabs[previousIndex].id);
}, [tabs, activeTabId, setActiveTab]);
const switchToTabByIndex = useCallback((index: number) => {
if (index >= 0 && index < tabs.length) {
setActiveTab(tabs[index].id);
}
}, [tabs, setActiveTab]);
const updateTabTitle = useCallback((id: string, title: string) => {
updateTab(id, { title });
}, [updateTab]);
const updateTabStatus = useCallback((id: string, status: Tab['status']) => {
updateTab(id, { status });
}, [updateTab]);
const markTabAsChanged = useCallback((id: string, hasChanges: boolean) => {
updateTab(id, { hasUnsavedChanges: hasChanges });
}, [updateTab]);
const findTabBySessionId = useCallback((sessionId: string): Tab | undefined => {
return tabs.find(tab => tab.type === 'chat' && tab.sessionId === sessionId);
}, [tabs]);
const findTabByAgentRunId = useCallback((agentRunId: string): Tab | undefined => {
return tabs.find(tab => tab.type === 'agent' && tab.agentRunId === agentRunId);
}, [tabs]);
const findTabByType = useCallback((type: Tab['type']): Tab | undefined => {
return tabs.find(tab => tab.type === type);
}, [tabs]);
const canAddTab = useCallback((): boolean => {
return tabs.length < 20; // MAX_TABS from context
}, [tabs.length]);
return {
// State
tabs,
activeTab,
activeTabId,
tabCount,
chatTabCount,
agentTabCount,
// Operations
createChatTab,
createAgentTab,
createAgentExecutionTab,
createProjectsTab,
createUsageTab,
createMCPTab,
createSettingsTab,
createClaudeMdTab,
createClaudeFileTab,
createCreateAgentTab,
createImportAgentTab,
closeTab,
closeCurrentTab,
switchToTab: setActiveTab,
switchToNextTab,
switchToPreviousTab,
switchToTabByIndex,
updateTab,
updateTabTitle,
updateTabStatus,
markTabAsChanged,
findTabBySessionId,
findTabByAgentRunId,
findTabByType,
canAddTab
};
};