import { create } from 'zustand' import { persist } from 'zustand/middleware' import { createLogger } from '../utils/logger' const logger = createLogger('TabStore') export type TabType = 'welcome' | 'terminal' | 'sftp' | 'portforward' | 'settings' export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error' // SSH 连接配置(用于快速连接) export interface SSHConnectionConfig { host: string port: number username: string password?: string privateKey?: string passphrase?: string } // 连接实例信息 export interface ConnectionInstance { connectionId: string // 实际的 SSH 连接 ID shellId?: string // Shell 会话 ID(仅 terminal 类型) sftpId?: string // SFTP 会话 ID(仅 sftp 类型) } export interface Tab { id: string title: string type: TabType sessionId?: string // 持久化会话 ID(用于获取配置) connectionConfig?: SSHConnectionConfig // 快速连接配置(临时) connectionInstance?: ConnectionInstance // 实际的连接实例(每个标签独立) isActive: boolean connectionStatus?: ConnectionStatus needsReconnect?: boolean // 从持久化恢复后需要重新连接 } // 用于持久化的序列化 Tab 数据(排除敏感字段和运行时状态) interface PersistedTab { id: string title: string type: TabType sessionId?: string // 注意:不持久化 connectionConfig(可能包含密码/密钥) // 注意:不持久化 connectionInstance(运行时状态) // 注意:不持久化 connectionStatus(运行时状态) // 注意:不持久化 isActive(由 activeTabId 决定) } interface TabState { tabs: Tab[] activeTabId: string | null // Actions addTab: (tab: Omit) => void updateTab: (tabId: string, updates: Partial) => void updateTabConnectionInstance: (tabId: string, instance: ConnectionInstance) => void updateTabStatus: (tabId: string, status: ConnectionStatus) => void closeTab: (tabId: string) => Tab | null // 返回被关闭的标签(用于清理连接) closeOtherTabs: (tabId: string) => Tab[] // 返回被关闭的标签数组 closeRightTabs: (tabId: string) => Tab[] closeLeftTabs: (tabId: string) => Tab[] closeAllTabs: () => Tab[] activateTab: (tabId: string) => void updateTabTitle: (tabId: string, title: string) => void getTabById: (tabId: string) => Tab | undefined } export const useTabStore = create()( persist( (set, get) => ({ tabs: [ { id: 'welcome', title: '欢迎', type: 'welcome', isActive: true } ], activeTabId: 'welcome', addTab: (tab) => { const { tabs } = get() // 检查是否是唯一类型的标签(settings, welcome),如果有则激活而不是创建新的 if (tab.type === 'settings' || tab.type === 'welcome') { const existing = tabs.find(t => t.type === tab.type) if (existing) { // 激活已存在的标签 const updatedTabs = tabs.map(t => ({ ...t, isActive: t.id === existing.id, })) set({ tabs: updatedTabs, activeTabId: existing.id }) return } } const newTab: Tab = { ...tab, id: `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, isActive: true, } // Deactivate all other tabs const updatedTabs = tabs.map(t => ({ ...t, isActive: false })) set({ tabs: [...updatedTabs, newTab], activeTabId: newTab.id, }) }, updateTab: (tabId, updates) => { const { tabs } = get() const updatedTabs = tabs.map(t => t.id === tabId ? { ...t, ...updates } : t ) set({ tabs: updatedTabs }) }, updateTabConnectionInstance: (tabId, instance) => { const { tabs } = get() const updatedTabs = tabs.map(t => t.id === tabId ? { ...t, connectionInstance: instance } : t ) set({ tabs: updatedTabs }) }, updateTabStatus: (tabId, status) => { const { tabs } = get() const updatedTabs = tabs.map(t => t.id === tabId ? { ...t, connectionStatus: status, needsReconnect: status === 'disconnected' ? false : t.needsReconnect } : t ) set({ tabs: updatedTabs }) }, closeTab: (tabId) => { const { tabs, activeTabId } = get() if (tabs.length <= 1) return null const closedTab = tabs.find(t => t.id === tabId) || null const newTabs = tabs.filter(t => t.id !== tabId) // If closing active tab, activate another one let newActiveId = activeTabId if (activeTabId === tabId) { const closedTabIndex = tabs.findIndex(t => t.id === tabId) const newActiveTab = newTabs[Math.min(closedTabIndex, newTabs.length - 1)] if (newActiveTab) { newActiveTab.isActive = true newActiveId = newActiveTab.id } } set({ tabs: newTabs, activeTabId: newActiveId }) return closedTab }, closeOtherTabs: (tabId) => { const { tabs } = get() const keepTab = tabs.find(t => t.id === tabId) if (!keepTab) return [] const closedTabs = tabs.filter(t => t.id !== tabId) keepTab.isActive = true set({ tabs: [keepTab], activeTabId: tabId, }) return closedTabs }, closeRightTabs: (tabId) => { const { tabs, activeTabId } = get() const tabIndex = tabs.findIndex(t => t.id === tabId) if (tabIndex === -1) return [] // 保留当前标签及其左侧所有标签 const closedTabs = tabs.slice(tabIndex + 1) const newTabs = tabs.slice(0, tabIndex + 1) // 如果关闭的是当前活动标签,激活最后一个 if (activeTabId && tabs.findIndex(t => t.id === activeTabId) > tabIndex) { newTabs[newTabs.length - 1].isActive = true set({ tabs: newTabs, activeTabId: newTabs[newTabs.length - 1].id }) } else { set({ tabs: newTabs }) } return closedTabs }, closeLeftTabs: (tabId) => { const { tabs, activeTabId } = get() const tabIndex = tabs.findIndex(t => t.id === tabId) if (tabIndex === -1) return [] // 保留当前标签及其右侧所有标签 const closedTabs = tabs.slice(0, tabIndex) const newTabs = tabs.slice(tabIndex) // 如果关闭的是当前活动标签,激活第一个 if (activeTabId && tabs.findIndex(t => t.id === activeTabId) < tabIndex) { newTabs[0].isActive = true set({ tabs: newTabs, activeTabId: newTabs[0].id }) } else { set({ tabs: newTabs, activeTabId: tabId }) } return closedTabs }, closeAllTabs: () => { const { tabs } = get() const welcomeTab: Tab = { id: 'welcome', title: '欢迎', type: 'welcome', isActive: true, } set({ tabs: [welcomeTab], activeTabId: 'welcome', }) return tabs }, activateTab: (tabId) => { const { tabs } = get() const updatedTabs = tabs.map(t => ({ ...t, isActive: t.id === tabId, })) set({ tabs: updatedTabs, activeTabId: tabId, }) }, updateTabTitle: (tabId, title) => { const { tabs } = get() const updatedTabs = tabs.map(t => t.id === tabId ? { ...t, title } : t ) set({ tabs: updatedTabs }) }, getTabById: (tabId) => { return get().tabs.find(t => t.id === tabId) }, }), { name: 'opsxshell-tabs', // 只持久化需要恢复的字段 partialize: (state) => { // 过滤掉 welcome 标签和 settings 标签(这些在启动时总会自动创建) // 只持久化有实际会话关联的标签(terminal, sftp, portforward) const tabsToPersist = state.tabs .filter(tab => { // 持久化有 sessionId 的标签或非 welcome/settings 类型 return tab.type === 'terminal' || tab.type === 'sftp' || tab.type === 'portforward' }) .map(tab => ({ id: tab.id, title: tab.title, type: tab.type, sessionId: tab.sessionId, // 不保存 connectionConfig(含密码等敏感信息) // 不保存 connectionInstance(运行时状态) // 不保存 connectionStatus(运行时状态) // 不保存 isActive(由 activeTabId 决定) })) as PersistedTab[] // 只持久化非 welcome 的 activeTabId const activeTab = state.tabs.find(t => t.id === state.activeTabId) let persistActiveTabId = state.activeTabId if (activeTab?.type === 'welcome') { persistActiveTabId = null } return { tabs: tabsToPersist, activeTabId: persistActiveTabId, } }, // 从持久化数据恢复时的合并策略 merge: (persistedState, currentState) => { const persisted = persistedState as Partial if (!persisted?.tabs || persisted.tabs.length === 0) { // 没有持久化数据,使用默认状态 return currentState } logger.info('Restoring tabs from persisted state:', persisted.tabs.length, 'tabs') // 将持久化的标签转换为运行时标签 const restoredTabs: Tab[] = persisted.tabs.map((tab: any) => ({ ...tab, isActive: false, connectionStatus: 'disconnected' as ConnectionStatus, needsReconnect: true, // 标记需要重新连接 })) // 确定活动标签 let activeTabId = persisted.activeTabId || null if (activeTabId) { const activeTab = restoredTabs.find(t => t.id === activeTabId) if (activeTab) { activeTab.isActive = true } else { // 活动标签不存在,选择第一个 if (restoredTabs.length > 0) { restoredTabs[0].isActive = true activeTabId = restoredTabs[0].id } } } else if (restoredTabs.length > 0) { restoredTabs[0].isActive = true activeTabId = restoredTabs[0].id } // 始终在开头添加 welcome 标签 const welcomeTab: Tab = { id: 'welcome', title: '欢迎', type: 'welcome', isActive: false, } const finalTabs = [welcomeTab, ...restoredTabs] // 如果活动标签不存在,默认激活 welcome if (!activeTabId) { welcomeTab.isActive = true activeTabId = 'welcome' } return { ...currentState, tabs: finalTabs, activeTabId, } }, } ) )