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

187
src/contexts/TabContext.tsx Normal file
View File

@@ -0,0 +1,187 @@
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
export interface Tab {
id: string;
type: 'chat' | 'agent' | 'projects' | 'usage' | 'mcp' | 'settings' | 'claude-md' | 'claude-file' | 'agent-execution' | 'create-agent' | 'import-agent';
title: string;
sessionId?: string; // for chat tabs
sessionData?: any; // for chat tabs - stores full session object
agentRunId?: string; // for agent tabs
agentData?: any; // for agent-execution tabs
claudeFileId?: string; // for claude-file tabs
initialProjectPath?: string; // for chat tabs
status: 'active' | 'idle' | 'running' | 'complete' | 'error';
hasUnsavedChanges: boolean;
order: number;
icon?: string;
createdAt: Date;
updatedAt: Date;
}
interface TabContextType {
tabs: Tab[];
activeTabId: string | null;
addTab: (tab: Omit<Tab, 'id' | 'order' | 'createdAt' | 'updatedAt'>) => string;
removeTab: (id: string) => void;
updateTab: (id: string, updates: Partial<Tab>) => void;
setActiveTab: (id: string) => void;
reorderTabs: (startIndex: number, endIndex: number) => void;
getTabById: (id: string) => Tab | undefined;
closeAllTabs: () => void;
getTabsByType: (type: 'chat' | 'agent') => Tab[];
}
const TabContext = createContext<TabContextType | undefined>(undefined);
// const STORAGE_KEY = 'claudia_tabs'; // No longer needed - persistence disabled
const MAX_TABS = 20;
export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTabId, setActiveTabId] = useState<string | null>(null);
// Always start with a fresh CC Projects tab
useEffect(() => {
// Create default projects tab
const defaultTab: Tab = {
id: generateTabId(),
type: 'projects',
title: 'CC Projects',
status: 'idle',
hasUnsavedChanges: false,
order: 0,
createdAt: new Date(),
updatedAt: new Date()
};
setTabs([defaultTab]);
setActiveTabId(defaultTab.id);
}, []);
// Tab persistence disabled - no longer saving to localStorage
// useEffect(() => {
// if (tabs.length > 0) {
// const tabsToSave = tabs.map(tab => ({
// ...tab,
// createdAt: tab.createdAt.toISOString(),
// updatedAt: tab.updatedAt.toISOString()
// }));
// localStorage.setItem(STORAGE_KEY, JSON.stringify(tabsToSave));
// }
// }, [tabs]);
const generateTabId = () => {
return `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
const addTab = useCallback((tabData: Omit<Tab, 'id' | 'order' | 'createdAt' | 'updatedAt'>): string => {
if (tabs.length >= MAX_TABS) {
throw new Error(`Maximum number of tabs (${MAX_TABS}) reached`);
}
const newTab: Tab = {
...tabData,
id: generateTabId(),
order: tabs.length,
createdAt: new Date(),
updatedAt: new Date()
};
setTabs(prevTabs => [...prevTabs, newTab]);
setActiveTabId(newTab.id);
return newTab.id;
}, [tabs.length]);
const removeTab = useCallback((id: string) => {
setTabs(prevTabs => {
const filteredTabs = prevTabs.filter(tab => tab.id !== id);
// Reorder remaining tabs
const reorderedTabs = filteredTabs.map((tab, index) => ({
...tab,
order: index
}));
// Update active tab if necessary
if (activeTabId === id && reorderedTabs.length > 0) {
const removedTabIndex = prevTabs.findIndex(tab => tab.id === id);
const newActiveIndex = Math.min(removedTabIndex, reorderedTabs.length - 1);
setActiveTabId(reorderedTabs[newActiveIndex].id);
} else if (reorderedTabs.length === 0) {
setActiveTabId(null);
}
return reorderedTabs;
});
}, [activeTabId]);
const updateTab = useCallback((id: string, updates: Partial<Tab>) => {
setTabs(prevTabs =>
prevTabs.map(tab =>
tab.id === id
? { ...tab, ...updates, updatedAt: new Date() }
: tab
)
);
}, []);
const setActiveTab = useCallback((id: string) => {
if (tabs.find(tab => tab.id === id)) {
setActiveTabId(id);
}
}, [tabs]);
const reorderTabs = useCallback((startIndex: number, endIndex: number) => {
setTabs(prevTabs => {
const newTabs = [...prevTabs];
const [removed] = newTabs.splice(startIndex, 1);
newTabs.splice(endIndex, 0, removed);
// Update order property
return newTabs.map((tab, index) => ({
...tab,
order: index
}));
});
}, []);
const getTabById = useCallback((id: string): Tab | undefined => {
return tabs.find(tab => tab.id === id);
}, [tabs]);
const closeAllTabs = useCallback(() => {
setTabs([]);
setActiveTabId(null);
// localStorage.removeItem(STORAGE_KEY); // Persistence disabled
}, []);
const getTabsByType = useCallback((type: 'chat' | 'agent'): Tab[] => {
return tabs.filter(tab => tab.type === type);
}, [tabs]);
const value: TabContextType = {
tabs,
activeTabId,
addTab,
removeTab,
updateTab,
setActiveTab,
reorderTabs,
getTabById,
closeAllTabs,
getTabsByType
};
return (
<TabContext.Provider value={value}>
{children}
</TabContext.Provider>
);
};
export const useTabContext = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabContext must be used within a TabProvider');
}
return context;
};