228 lines
6.4 KiB
TypeScript
228 lines
6.4 KiB
TypeScript
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
|
|
export interface Tab {
|
|
id: string;
|
|
type: 'chat' | 'agent' | 'projects' | 'usage' | 'mcp' | 'settings' | '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 { t } = useTranslation();
|
|
const [tabs, setTabs] = useState<Tab[]>([]);
|
|
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
|
|
|
|
|
// Ensure there is always at least one projects tab, but avoid clobbering existing tabs
|
|
useEffect(() => {
|
|
if (tabs.length > 0) {
|
|
return;
|
|
}
|
|
|
|
const defaultTab: Tab = {
|
|
id: generateTabId(),
|
|
type: 'projects',
|
|
title: t('ccProjects'),
|
|
status: 'idle',
|
|
hasUnsavedChanges: false,
|
|
order: 0,
|
|
icon: 'folder',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
};
|
|
|
|
setTabs([defaultTab]);
|
|
setActiveTabId(defaultTab.id);
|
|
}, [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(() => {
|
|
// 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(t('maximumTabsReached', { max: MAX_TABS }));
|
|
}
|
|
|
|
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, t]);
|
|
|
|
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) => {
|
|
const tabExists = tabs.find(tab => tab.id === id);
|
|
if (tabExists) {
|
|
setActiveTabId(id);
|
|
}
|
|
}, [tabs, activeTabId]);
|
|
|
|
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;
|
|
};
|