Files
claudia/src/contexts/TabContext.tsx
2025-10-22 14:55:28 +08:00

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;
};