mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat: unify Settings/Scheduled as tab pages, improve new session UX
- Settings and Scheduled Tasks now open as special tabs in the TabBar, matching IDE conventions (closeable, icon-prefixed, persistent) - New Session directly creates a session with the current workDir - ChatInput shows DirectoryPicker before first message, locks after - Remove connection status from StatusBar (unnecessary noise) - ContentRouter routes by tab type instead of activeView - Settings page no longer has a back-button header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2fa6fc56d
commit
7e2c8803f7
@ -38,7 +38,7 @@ describe('Content-only pages render without errors', () => {
|
||||
|
||||
it('ActiveSession renders with chat components', () => {
|
||||
const SESSION_ID = 'test-active-session'
|
||||
useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', status: 'idle' }], activeTabId: SESSION_ID })
|
||||
useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID })
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[SESSION_ID]: {
|
||||
@ -71,7 +71,7 @@ describe('Content-only pages render without errors', () => {
|
||||
})
|
||||
|
||||
it('ActiveSession shows a single primary action button while a turn is active', () => {
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] })
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] })
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'active-tab': {
|
||||
|
||||
@ -9,6 +9,7 @@ import { ModelSelector } from '../controls/ModelSelector'
|
||||
import type { AttachmentRef } from '../../types/chat'
|
||||
import { AttachmentGallery } from './AttachmentGallery'
|
||||
import { ProjectContextChip } from '../shared/ProjectContextChip'
|
||||
import { DirectoryPicker } from '../shared/DirectoryPicker'
|
||||
import { FileSearchMenu, type FileSearchMenuHandle } from './FileSearchMenu'
|
||||
import {
|
||||
FALLBACK_SLASH_COMMANDS,
|
||||
@ -49,9 +50,10 @@ export function ChatInput() {
|
||||
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const slashCommands = sessionState?.slashCommands ?? []
|
||||
const activeSessionId = useSessionStore((state) => state.activeSessionId)
|
||||
const activeSession = useSessionStore((state) => state.sessions.find((session) => session.id === state.activeSessionId) ?? null)
|
||||
const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : null)
|
||||
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null)
|
||||
const messages = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.messages ?? [] : [])
|
||||
const hasMessages = messages.length > 0
|
||||
|
||||
const isActive = chatState !== 'idle'
|
||||
const isWorkspaceMissing = activeSession?.workDirExists === false
|
||||
@ -62,12 +64,12 @@ export function ChatInput() {
|
||||
}, [isActive])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeSessionId) {
|
||||
if (!activeTabId) {
|
||||
setGitInfo(null)
|
||||
return
|
||||
}
|
||||
sessionsApi.getGitInfo(activeSessionId).then(setGitInfo).catch(() => setGitInfo(null))
|
||||
}, [activeSessionId])
|
||||
sessionsApi.getGitInfo(activeTabId).then(setGitInfo).catch(() => setGitInfo(null))
|
||||
}, [activeTabId])
|
||||
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
@ -518,11 +520,30 @@ export function ChatInput() {
|
||||
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileSelect} />
|
||||
|
||||
<div className="mt-3 px-1">
|
||||
<ProjectContextChip
|
||||
workDir={gitInfo?.workDir || activeSession?.workDir}
|
||||
repoName={gitInfo?.repoName || null}
|
||||
branch={gitInfo?.branch || null}
|
||||
/>
|
||||
{hasMessages ? (
|
||||
<ProjectContextChip
|
||||
workDir={gitInfo?.workDir || activeSession?.workDir}
|
||||
repoName={gitInfo?.repoName || null}
|
||||
branch={gitInfo?.branch || null}
|
||||
/>
|
||||
) : (
|
||||
<DirectoryPicker
|
||||
value={gitInfo?.workDir || activeSession?.workDir || ''}
|
||||
onChange={async (newWorkDir) => {
|
||||
if (!activeTabId) return
|
||||
// Recreate session with new workDir
|
||||
const { deleteSession, createSession } = useSessionStore.getState()
|
||||
const { closeTab, openTab } = useTabStore.getState()
|
||||
const { disconnectSession, connectToSession } = useChatStore.getState()
|
||||
disconnectSession(activeTabId)
|
||||
closeTab(activeTabId)
|
||||
await deleteSession(activeTabId)
|
||||
const newId = await createSession(newWorkDir)
|
||||
openTab(newId, t('sidebar.newSession'))
|
||||
connectToSession(newId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -30,7 +30,7 @@ function makeSessionState(overrides: Partial<PerSessionState> = {}): PerSessionS
|
||||
|
||||
describe('MessageList nested tool calls', () => {
|
||||
beforeEach(() => {
|
||||
useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', status: 'idle' }] })
|
||||
useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', type: 'session' as const, status: 'idle' }] })
|
||||
useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState() } })
|
||||
})
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { useTabStore } from '../../stores/tabStore'
|
||||
|
||||
describe('chat blocks', () => {
|
||||
beforeEach(() => {
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] })
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] })
|
||||
useChatStore.setState({ sessions: {} })
|
||||
})
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
|
||||
import { initializeDesktopServerUrl } from '../../lib/desktopRuntime'
|
||||
import { TabBar } from './TabBar'
|
||||
import { StatusBar } from './StatusBar'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
|
||||
@ -103,6 +104,7 @@ export function AppShell() {
|
||||
<main id="content-area" className={`flex-1 flex flex-col overflow-hidden relative ${isTauri ? 'pt-[38px]' : ''}`}>
|
||||
<TabBar />
|
||||
<ContentRouter />
|
||||
<StatusBar />
|
||||
</main>
|
||||
<ToastContainer />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTeamStore } from '../../stores/teamStore'
|
||||
import { EmptySession } from '../../pages/EmptySession'
|
||||
@ -8,62 +7,28 @@ import { Settings } from '../../pages/Settings'
|
||||
import { AgentTranscript } from '../../pages/AgentTranscript'
|
||||
|
||||
export function ContentRouter() {
|
||||
const activeView = useUIStore((s) => s.activeView)
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const activeTab = useTabStore((s) => s.tabs.find((t) => t.sessionId === s.activeTabId))
|
||||
const viewingAgentId = useTeamStore((s) => s.viewingAgentId)
|
||||
|
||||
if (activeView === 'settings') {
|
||||
return <Settings />
|
||||
}
|
||||
|
||||
if (activeView === 'scheduled') {
|
||||
return <ScheduledTasks />
|
||||
}
|
||||
|
||||
if (activeView === 'terminal') {
|
||||
return <TerminalPlaceholder />
|
||||
}
|
||||
|
||||
if (activeView === 'history') {
|
||||
// If viewing an agent transcript, show that
|
||||
if (viewingAgentId) {
|
||||
return <AgentTranscript />
|
||||
}
|
||||
return <HistoryPlaceholder />
|
||||
}
|
||||
|
||||
// Code view
|
||||
if (!activeTabId) {
|
||||
// No tabs open — show empty session
|
||||
if (!activeTabId || !activeTab) {
|
||||
return <EmptySession />
|
||||
}
|
||||
|
||||
// Special tabs
|
||||
if (activeTab.type === 'settings') {
|
||||
return <Settings />
|
||||
}
|
||||
|
||||
if (activeTab.type === 'scheduled') {
|
||||
return <ScheduledTasks />
|
||||
}
|
||||
|
||||
// Session tab — show agent transcript or active session
|
||||
if (viewingAgentId) {
|
||||
return <AgentTranscript />
|
||||
}
|
||||
|
||||
return <ActiveSession />
|
||||
}
|
||||
|
||||
function TerminalPlaceholder() {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-[#1e1e1e] text-[#d4d4d4]">
|
||||
<span className="material-symbols-outlined text-[48px] text-[#555] mb-4">terminal</span>
|
||||
<h2 className="text-lg font-semibold text-[#999] mb-2">Terminal</h2>
|
||||
<p className="text-sm text-[#666] max-w-sm text-center">
|
||||
Integrated terminal coming soon. Use the Code tab to interact with Claude.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HistoryPlaceholder() {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center bg-[var(--color-surface)]">
|
||||
<span className="material-symbols-outlined text-[48px] text-[var(--color-outline)] mb-4">history</span>
|
||||
<h2 className="text-lg font-semibold text-[var(--color-text-secondary)] mb-2">History</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)] max-w-sm text-center">
|
||||
View session history and agent transcripts. Select a session from the sidebar to view its history.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useUIStore } from '../../stores/uiStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { ProjectFilter } from './ProjectFilter'
|
||||
import type { SessionListItem } from '../../types/session'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tabStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
|
||||
const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window)
|
||||
@ -16,15 +15,13 @@ const TIME_GROUP_ORDER: TimeGroup[] = ['today', 'yesterday', 'last7days', 'last3
|
||||
export function Sidebar() {
|
||||
const {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
selectedProjects,
|
||||
error,
|
||||
setActiveSession,
|
||||
fetchSessions,
|
||||
deleteSession,
|
||||
renameSession,
|
||||
} = useSessionStore()
|
||||
const { activeView, setActiveView } = useUIStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null)
|
||||
@ -132,15 +129,29 @@ export function Sidebar() {
|
||||
{/* Navigation */}
|
||||
<div className="px-3 pb-3 flex flex-col gap-0.5">
|
||||
<NavItem
|
||||
active={activeView === 'code' && !activeSessionId}
|
||||
onClick={() => { setActiveView('code'); setActiveSession(null) }}
|
||||
active={false}
|
||||
onClick={async () => {
|
||||
try {
|
||||
// Use current active session's workDir as default for new session
|
||||
const currentTabId = useTabStore.getState().activeTabId
|
||||
const currentSession = currentTabId
|
||||
? useSessionStore.getState().sessions.find((s) => s.id === currentTabId)
|
||||
: null
|
||||
const workDir = currentSession?.workDir || undefined
|
||||
const sessionId = await useSessionStore.getState().createSession(workDir)
|
||||
useTabStore.getState().openTab(sessionId, t('sidebar.newSession'))
|
||||
useChatStore.getState().connectToSession(sessionId)
|
||||
} catch {
|
||||
// Session creation failed — no tab opened
|
||||
}
|
||||
}}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t('sidebar.newSession')}
|
||||
</NavItem>
|
||||
<NavItem
|
||||
active={activeView === 'scheduled'}
|
||||
onClick={() => setActiveView('scheduled')}
|
||||
active={activeTabId === SCHEDULED_TAB_ID}
|
||||
onClick={() => useTabStore.getState().openTab(SCHEDULED_TAB_ID, t('sidebar.scheduled'), 'scheduled')}
|
||||
icon={<ClockIcon />}
|
||||
>
|
||||
{t('sidebar.scheduled')}
|
||||
@ -208,22 +219,21 @@ export function Sidebar() {
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveView('code')
|
||||
useTabStore.getState().openTab(session.id, session.title)
|
||||
useChatStore.getState().connectToSession(session.id)
|
||||
}}
|
||||
onContextMenu={(e) => handleContextMenu(e, session.id)}
|
||||
className={`
|
||||
w-full flex items-center gap-2 pl-4 pr-3 py-1.5 text-sm text-left rounded-[var(--radius-md)] transition-colors duration-200 group
|
||||
${session.id === activeSessionId
|
||||
${session.id === activeTabId
|
||||
? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full flex-shrink-0" style={{
|
||||
backgroundColor: session.id === activeSessionId ? 'var(--color-brand)' : 'var(--color-text-tertiary)',
|
||||
opacity: session.id === activeSessionId ? 1 : 0.5,
|
||||
backgroundColor: session.id === activeTabId ? 'var(--color-brand)' : 'var(--color-text-tertiary)',
|
||||
opacity: session.id === activeTabId ? 1 : 0.5,
|
||||
}} />
|
||||
<span className="truncate flex-1">{session.title || 'Untitled'}</span>
|
||||
{!session.workDirExists && (
|
||||
@ -249,8 +259,8 @@ export function Sidebar() {
|
||||
{/* Settings button at bottom */}
|
||||
<div className="p-3 border-t border-[var(--color-border)]">
|
||||
<NavItem
|
||||
active={activeView === 'settings'}
|
||||
onClick={() => setActiveView('settings')}
|
||||
active={activeTabId === SETTINGS_TAB_ID}
|
||||
onClick={() => useTabStore.getState().openTab(SETTINGS_TAB_ID, t('sidebar.settings'), 'settings')}
|
||||
icon={<span className="material-symbols-outlined text-[18px]">settings</span>}
|
||||
>
|
||||
{t('sidebar.settings')}
|
||||
|
||||
@ -1,33 +1,13 @@
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
export function StatusBar() {
|
||||
const { currentModel } = useSettingsStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const connectionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.connectionState ?? 'disconnected' : 'disconnected')
|
||||
const { sessions, activeSessionId } = useSessionStore()
|
||||
const t = useTranslation()
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
||||
|
||||
const statusColor =
|
||||
connectionState === 'connected'
|
||||
? 'bg-[var(--color-success)]'
|
||||
: connectionState === 'connecting' || connectionState === 'reconnecting'
|
||||
? 'bg-[var(--color-warning)] animate-pulse-dot'
|
||||
: 'bg-[var(--color-error)]'
|
||||
|
||||
const statusText =
|
||||
connectionState === 'connected'
|
||||
? t('status.connected')
|
||||
: connectionState === 'connecting'
|
||||
? t('status.connecting')
|
||||
: connectionState === 'reconnecting'
|
||||
? t('status.reconnecting')
|
||||
: t('status.disconnected')
|
||||
const activeSession = sessions.find((s) => s.id === activeTabId)
|
||||
|
||||
const projectName = activeSession?.projectPath
|
||||
? activeSession.projectPath.split('-').filter(Boolean).pop() || ''
|
||||
@ -35,18 +15,9 @@ export function StatusBar() {
|
||||
|
||||
return (
|
||||
<div className="h-[var(--statusbar-height)] flex items-center justify-between px-4 border-t border-[var(--color-border)] bg-[var(--color-surface-sidebar)] select-none text-[11px]">
|
||||
{/* Left: User info + plan */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${statusColor}`} />
|
||||
<span className="text-[var(--color-text-tertiary)]">{statusText}</span>
|
||||
</div>
|
||||
|
||||
{projectName && (
|
||||
<>
|
||||
<span className="text-[var(--color-outline)]">·</span>
|
||||
<span className="text-[var(--color-text-secondary)] font-[var(--font-mono)]">{projectName}</span>
|
||||
</>
|
||||
<span className="text-[var(--color-text-secondary)] font-[var(--font-mono)]">{projectName}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -53,6 +53,13 @@ export function TabBar() {
|
||||
}
|
||||
|
||||
const handleClose = (sessionId: string) => {
|
||||
// Special tabs can always be closed directly
|
||||
const tab = tabs.find((t) => t.sessionId === sessionId)
|
||||
if (tab && tab.type !== 'session') {
|
||||
closeTab(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
const sessionState = useChatStore.getState().sessions[sessionId]
|
||||
const isRunning = sessionState && sessionState.chatState !== 'idle'
|
||||
|
||||
@ -198,12 +205,18 @@ function TabItem({ tab, isActive, onClick, onClose, onContextMenu }: {
|
||||
`}
|
||||
style={{ width: TAB_WIDTH, maxWidth: TAB_WIDTH }}
|
||||
>
|
||||
{tab.status === 'running' && (
|
||||
{tab.type === 'session' && tab.status === 'running' && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-success)] animate-pulse flex-shrink-0" />
|
||||
)}
|
||||
{tab.status === 'error' && (
|
||||
{tab.type === 'session' && tab.status === 'error' && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-error)] flex-shrink-0" />
|
||||
)}
|
||||
{tab.type === 'settings' && (
|
||||
<span className="material-symbols-outlined text-[14px] flex-shrink-0 text-[var(--color-text-tertiary)]">settings</span>
|
||||
)}
|
||||
{tab.type === 'scheduled' && (
|
||||
<span className="material-symbols-outlined text-[14px] flex-shrink-0 text-[var(--color-text-tertiary)]">schedule</span>
|
||||
)}
|
||||
|
||||
<span className={`flex-1 truncate text-xs ${isActive ? 'text-[var(--color-text-primary)] font-medium' : 'text-[var(--color-text-secondary)]'}`}>
|
||||
{tab.title || 'Untitled'}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useProviderStore } from '../stores/providerStore'
|
||||
import { useUIStore } from '../stores/uiStore'
|
||||
import { useTranslation } from '../i18n'
|
||||
import { Modal } from '../components/shared/Modal'
|
||||
import { Input } from '../components/shared/Input'
|
||||
@ -17,22 +16,10 @@ type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters'
|
||||
|
||||
export function Settings() {
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('providers')
|
||||
const setActiveView = useUIStore((s) => s.setActiveView)
|
||||
const t = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--color-surface)]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 px-6 py-4 border-b border-[var(--color-border)]">
|
||||
<button
|
||||
onClick={() => setActiveView('code')}
|
||||
className="p-1.5 rounded-lg hover:bg-[var(--color-surface-hover)] transition-colors text-[var(--color-text-secondary)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">arrow_back</span>
|
||||
</button>
|
||||
<h1 className="text-lg font-bold text-[var(--color-text-primary)]">{t('settings.title')}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Tab navigation */}
|
||||
<div className="w-48 border-r border-[var(--color-border)] py-3 flex-shrink-0">
|
||||
|
||||
@ -3,14 +3,20 @@ import { sessionsApi } from '../api/sessions'
|
||||
|
||||
const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
|
||||
|
||||
export const SETTINGS_TAB_ID = '__settings__'
|
||||
export const SCHEDULED_TAB_ID = '__scheduled__'
|
||||
|
||||
export type TabType = 'session' | 'settings' | 'scheduled'
|
||||
|
||||
export type Tab = {
|
||||
sessionId: string
|
||||
title: string
|
||||
type: TabType
|
||||
status: 'idle' | 'running' | 'error'
|
||||
}
|
||||
|
||||
type TabPersistence = {
|
||||
openTabs: Array<{ sessionId: string; title: string }>
|
||||
openTabs: Array<{ sessionId: string; title: string; type?: TabType }>
|
||||
activeTabId: string | null
|
||||
}
|
||||
|
||||
@ -18,7 +24,7 @@ type TabStore = {
|
||||
tabs: Tab[]
|
||||
activeTabId: string | null
|
||||
|
||||
openTab: (sessionId: string, title: string) => void
|
||||
openTab: (sessionId: string, title: string, type?: TabType) => void
|
||||
closeTab: (sessionId: string) => void
|
||||
setActiveTab: (sessionId: string) => void
|
||||
updateTabTitle: (sessionId: string, title: string) => void
|
||||
@ -32,14 +38,14 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
|
||||
openTab: (sessionId, title) => {
|
||||
openTab: (sessionId, title, type = 'session') => {
|
||||
const { tabs } = get()
|
||||
const existing = tabs.find((t) => t.sessionId === sessionId)
|
||||
if (existing) {
|
||||
set({ activeTabId: sessionId })
|
||||
} else {
|
||||
set({
|
||||
tabs: [...tabs, { sessionId, title, status: 'idle' }],
|
||||
tabs: [...tabs, { sessionId, title, type, status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
}
|
||||
@ -89,7 +95,7 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
saveTabs: () => {
|
||||
const { tabs, activeTabId } = get()
|
||||
const data: TabPersistence = {
|
||||
openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title })),
|
||||
openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title, type: t.type })),
|
||||
activeTabId,
|
||||
}
|
||||
try {
|
||||
@ -109,12 +115,23 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
const existingIds = new Set(sessions.map((s) => s.id))
|
||||
|
||||
const validTabs: Tab[] = data.openTabs
|
||||
.filter((t) => existingIds.has(t.sessionId))
|
||||
.map((t) => ({
|
||||
sessionId: t.sessionId,
|
||||
title: sessions.find((s) => s.id === t.sessionId)?.title || t.title,
|
||||
status: 'idle' as const,
|
||||
}))
|
||||
.filter((t) => {
|
||||
// Special tabs are always valid
|
||||
if (t.type === 'settings' || t.type === 'scheduled') return true
|
||||
// Session tabs must exist on server
|
||||
return existingIds.has(t.sessionId)
|
||||
})
|
||||
.map((t) => {
|
||||
if (t.type === 'settings' || t.type === 'scheduled') {
|
||||
return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
|
||||
}
|
||||
return {
|
||||
sessionId: t.sessionId,
|
||||
title: sessions.find((s) => s.id === t.sessionId)?.title || t.title,
|
||||
type: 'session' as const,
|
||||
status: 'idle' as const,
|
||||
}
|
||||
})
|
||||
|
||||
if (validTabs.length === 0) return
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user