mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: restore settings after skill center split
Tested: cd desktop && bun run test -- skillsSettings.test.tsx tabStore.test.ts uiStore.test.ts persistenceMigrations.test.ts TabBar.test.tsx SkillCenter.test.tsx --run Tested: bun run check:desktop Tested: git diff --check Not-tested: full bun run verify was not run for this scoped desktop fix. Confidence: high Scope-risk: narrow
This commit is contained in:
parent
15b3acc85b
commit
ce7b2b4292
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@ -51,6 +51,7 @@ vi.mock('../stores/agentStore', () => ({
|
||||
describe('Settings > Skills compatibility entry', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
useSettingsStore.setState({ locale: 'en' })
|
||||
useTabStore.setState(useTabStore.getInitialState(), true)
|
||||
useUIStore.setState({
|
||||
@ -59,21 +60,30 @@ describe('Settings > Skills compatibility entry', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('opens the unified Skill Center from the legacy settings tab button', () => {
|
||||
it('does not keep the removed Skills entry inside Settings navigation', () => {
|
||||
render(<Settings />)
|
||||
|
||||
fireEvent.click(screen.getByText('Skills'))
|
||||
|
||||
expect(useTabStore.getState().activeTabId).toBe(SKILL_CENTER_TAB_ID)
|
||||
expect(useTabStore.getState().tabs).toContainEqual({
|
||||
sessionId: SKILL_CENTER_TAB_ID,
|
||||
title: 'Skills',
|
||||
type: 'skill-center',
|
||||
status: 'idle',
|
||||
})
|
||||
expect(screen.queryByRole('button', { name: 'Skills' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('redirects pending legacy skills settings navigation to the Skill Center', async () => {
|
||||
it('normalizes a legacy active Skills settings tab back to General', async () => {
|
||||
const fetchOutputStyles = vi.fn().mockResolvedValue(undefined)
|
||||
useSettingsStore.setState({ fetchOutputStyles })
|
||||
useUIStore.setState({
|
||||
activeSettingsTab: 'skills',
|
||||
pendingSettingsTab: null,
|
||||
})
|
||||
|
||||
render(<Settings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useUIStore.getState().activeSettingsTab).toBe('general')
|
||||
})
|
||||
expect(useTabStore.getState().activeTabId).not.toBe(SKILL_CENTER_TAB_ID)
|
||||
expect(fetchOutputStyles).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('redirects pending legacy skills settings navigation to the Skill Center without persisting it', async () => {
|
||||
useUIStore.setState({
|
||||
activeSettingsTab: 'providers',
|
||||
pendingSettingsTab: 'skills',
|
||||
@ -85,5 +95,7 @@ describe('Settings > Skills compatibility entry', () => {
|
||||
expect(useTabStore.getState().activeTabId).toBe(SKILL_CENTER_TAB_ID)
|
||||
})
|
||||
expect(useUIStore.getState().pendingSettingsTab).toBeNull()
|
||||
expect(useUIStore.getState().activeSettingsTab).toBe('providers')
|
||||
expect(localStorage.getItem('cc-haha-active-settings-tab')).not.toBe('skills')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1217,6 +1217,36 @@ describe('TabBar', () => {
|
||||
expect(useTabStore.getState().tabs).toEqual([])
|
||||
})
|
||||
|
||||
it('closes the skill center tab from the close button without disconnecting chat sessions', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { SKILL_CENTER_TAB_ID, useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
|
||||
const disconnectSession = vi.fn()
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [
|
||||
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: SKILL_CENTER_TAB_ID, title: 'Skills', type: 'skill-center', status: 'idle' },
|
||||
],
|
||||
activeTabId: SKILL_CENTER_TAB_ID,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
disconnectSession,
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Close Skills'))
|
||||
|
||||
expect(disconnectSession).not.toHaveBeenCalled()
|
||||
expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-1'])
|
||||
expect(useTabStore.getState().activeTabId).toBe('tab-1')
|
||||
})
|
||||
|
||||
it('opens the bottom terminal panel from the toolbar for an active session', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
|
||||
@ -636,9 +636,9 @@ const TabItem = forwardRef<HTMLDivElement, {
|
||||
aria-label={`Close ${tab.title || 'Untitled'}`}
|
||||
onMouseDown={(e) => { e.stopPropagation() }}
|
||||
onClick={(e) => { e.stopPropagation(); onClose() }}
|
||||
className="flex-shrink-0 -mr-0.5 inline-flex h-3 w-3 items-center justify-center bg-transparent p-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-[opacity,color] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] focus-visible:outline-none"
|
||||
className="flex-shrink-0 -mr-1 inline-flex h-6 w-6 items-center justify-center rounded-md bg-transparent p-0 opacity-0 transition-[background-color,opacity,color] text-[var(--color-text-tertiary)] group-hover:opacity-100 hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-secondary)] focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[11px] leading-none">close</span>
|
||||
<span className="material-symbols-outlined text-[13px] leading-none">close</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -6,7 +6,7 @@ import { useUIStore } from '../../stores/uiStore'
|
||||
import { Button } from '../shared/Button'
|
||||
import { ConfirmDialog } from '../shared/ConfirmDialog'
|
||||
import type { PluginCapabilityKey } from '../../types/plugin'
|
||||
import { SETTINGS_TAB_ID, useTabStore } from '../../stores/tabStore'
|
||||
import { SETTINGS_TAB_ID, SKILL_CENTER_TAB_ID, useTabStore } from '../../stores/tabStore'
|
||||
import { useSkillStore } from '../../stores/skillStore'
|
||||
import { useAgentStore } from '../../stores/agentStore'
|
||||
import { useMcpStore } from '../../stores/mcpStore'
|
||||
@ -101,7 +101,7 @@ export function PluginDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const openSettingsTab = (tab: 'skills' | 'agents' | 'mcp') => {
|
||||
const openSettingsTab = (tab: 'agents' | 'mcp') => {
|
||||
useUIStore.getState().setPendingSettingsTab(tab)
|
||||
useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings')
|
||||
}
|
||||
@ -114,7 +114,7 @@ export function PluginDetail() {
|
||||
})
|
||||
return
|
||||
}
|
||||
openSettingsTab('skills')
|
||||
useTabStore.getState().openTab(SKILL_CENTER_TAB_ID, t('skillCenter.title'), 'skill-center')
|
||||
await fetchSkillDetail('plugin', skillName, currentWorkDir, 'plugins')
|
||||
|
||||
const { selectedSkill, error } = useSkillStore.getState()
|
||||
|
||||
@ -48,6 +48,26 @@ describe('desktop persistence migrations', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('canonicalizes mismatched persisted special tab ids and types during startup migration', () => {
|
||||
window.localStorage.setItem('cc-haha-open-tabs', JSON.stringify({
|
||||
openTabs: [
|
||||
{ sessionId: '__settings__', title: 'Settings', type: 'skill-center' },
|
||||
{ sessionId: '__skill_center__', title: 'Skills', type: 'settings' },
|
||||
],
|
||||
activeTabId: '__settings__',
|
||||
}))
|
||||
|
||||
runDesktopPersistenceMigrations()
|
||||
|
||||
expect(JSON.parse(window.localStorage.getItem('cc-haha-open-tabs') || '{}')).toEqual({
|
||||
openTabs: [
|
||||
{ sessionId: '__settings__', title: 'Settings', type: 'settings' },
|
||||
{ sessionId: '__skill_center__', title: 'Skills', type: 'skill-center' },
|
||||
],
|
||||
activeTabId: '__settings__',
|
||||
})
|
||||
})
|
||||
|
||||
test('filters stale session runtime selections without clearing unrelated keys', () => {
|
||||
window.localStorage.setItem('unrelated-user-key', 'keep')
|
||||
window.localStorage.setItem('cc-haha-session-runtime', JSON.stringify({
|
||||
|
||||
@ -21,6 +21,12 @@ const THEME_STORAGE_KEY = 'cc-haha-theme'
|
||||
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
|
||||
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max']
|
||||
const PERSISTED_SPECIAL_TAB_TYPES = ['settings', 'scheduled', 'skill-center', 'traces'] as const
|
||||
const PERSISTED_SPECIAL_TAB_IDS: Record<(typeof PERSISTED_SPECIAL_TAB_TYPES)[number], string> = {
|
||||
settings: '__settings__',
|
||||
scheduled: '__scheduled__',
|
||||
'skill-center': '__skill_center__',
|
||||
traces: '__traces__',
|
||||
}
|
||||
const SUPPORTED_LOCALES = ['en', 'zh', 'zh-TW', 'jp', 'kr']
|
||||
|
||||
function readJson(storage: StorageLike, key: string): unknown {
|
||||
@ -37,6 +43,14 @@ function isPersistedSpecialTabType(value: unknown): value is (typeof PERSISTED_S
|
||||
return typeof value === 'string' && (PERSISTED_SPECIAL_TAB_TYPES as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
function getPersistedSpecialTabType(tab: Record<string, unknown>): (typeof PERSISTED_SPECIAL_TAB_TYPES)[number] | null {
|
||||
if (tab.sessionId === '__settings__') return 'settings'
|
||||
if (tab.sessionId === '__scheduled__') return 'scheduled'
|
||||
if (tab.sessionId === '__skill_center__') return 'skill-center'
|
||||
if (tab.sessionId === '__traces__') return 'traces'
|
||||
return isPersistedSpecialTabType(tab.type) ? tab.type : null
|
||||
}
|
||||
|
||||
function writeJson(storage: StorageLike, key: string, value: unknown): void {
|
||||
storage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
@ -56,13 +70,14 @@ function migrateTabs(storage: StorageLike, report: DesktopMigrationReport): void
|
||||
.filter((tab): tab is Record<string, unknown> => isRecord(tab))
|
||||
.filter((tab) => typeof tab.sessionId === 'string' && typeof tab.title === 'string')
|
||||
.filter((tab) => tab.type !== 'terminal' && !String(tab.sessionId).startsWith('__terminal__'))
|
||||
.map((tab) => ({
|
||||
sessionId: tab.sessionId as string,
|
||||
title: tab.title as string,
|
||||
type: isPersistedSpecialTabType(tab.type)
|
||||
? tab.type
|
||||
: 'session',
|
||||
}))
|
||||
.map((tab) => {
|
||||
const specialType = getPersistedSpecialTabType(tab)
|
||||
return {
|
||||
sessionId: specialType ? PERSISTED_SPECIAL_TAB_IDS[specialType] : tab.sessionId as string,
|
||||
title: tab.title as string,
|
||||
type: specialType ?? 'session',
|
||||
}
|
||||
})
|
||||
const activeTabId =
|
||||
isRecord(parsed) &&
|
||||
typeof parsed.activeTabId === 'string' &&
|
||||
|
||||
@ -194,21 +194,27 @@ function buildH5PublicBaseUrlFromHostDraft(draft: string, currentBaseUrl: string
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const activeTab = useUIStore((s) => s.activeSettingsTab)
|
||||
const activeSettingsTab = useUIStore((s) => s.activeSettingsTab)
|
||||
const activeTab = activeSettingsTab === 'skills' ? 'general' : activeSettingsTab
|
||||
const setActiveTab = useUIStore((s) => s.setActiveSettingsTab)
|
||||
const pendingSettingsTab = useUIStore((s) => s.pendingSettingsTab)
|
||||
const t = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingSettingsTab) return
|
||||
if (pendingSettingsTab === 'skills') {
|
||||
useUIStore.getState().setPendingSettingsTab(null)
|
||||
useTabStore.getState().openTab(SKILL_CENTER_TAB_ID, t('skillCenter.title'), 'skill-center')
|
||||
return
|
||||
}
|
||||
setActiveTab(pendingSettingsTab)
|
||||
useUIStore.getState().setPendingSettingsTab(null)
|
||||
}, [pendingSettingsTab, setActiveTab])
|
||||
}, [pendingSettingsTab, setActiveTab, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'skills') return
|
||||
useTabStore.getState().openTab(SKILL_CENTER_TAB_ID, t('skillCenter.title'), 'skill-center')
|
||||
}, [activeTab, t])
|
||||
if (activeSettingsTab !== 'skills') return
|
||||
setActiveTab('general')
|
||||
}, [activeSettingsTab, setActiveTab])
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--color-surface)]">
|
||||
@ -223,12 +229,6 @@ export function Settings() {
|
||||
<TabButton icon="terminal" label={t('settings.tab.terminal')} active={activeTab === 'terminal'} onClick={() => setActiveTab('terminal')} />
|
||||
<TabButton icon="dns" label={t('settings.tab.mcp')} active={activeTab === 'mcp'} onClick={() => setActiveTab('mcp')} />
|
||||
<TabButton icon="smart_toy" label={t('settings.tab.agents')} active={activeTab === 'agents'} onClick={() => setActiveTab('agents')} />
|
||||
<TabButton
|
||||
icon="auto_awesome"
|
||||
label={t('settings.tab.skills')}
|
||||
active={activeTab === 'skills'}
|
||||
onClick={() => useTabStore.getState().openTab(SKILL_CENTER_TAB_ID, t('skillCenter.title'), 'skill-center')}
|
||||
/>
|
||||
<TabButton icon="history_edu" label={t('settings.tab.memory')} active={activeTab === 'memory'} onClick={() => setActiveTab('memory')} />
|
||||
<TabButton icon="extension" label={t('settings.tab.plugins')} active={activeTab === 'plugins'} onClick={() => setActiveTab('plugins')} />
|
||||
<TabButton icon="mouse" label={t('settings.tab.computerUse')} active={activeTab === 'computerUse'} onClick={() => setActiveTab('computerUse')} />
|
||||
@ -251,7 +251,6 @@ export function Settings() {
|
||||
{activeTab === 'terminal' && <TerminalSettings showPreferences />}
|
||||
{activeTab === 'mcp' && <McpSettings />}
|
||||
{activeTab === 'agents' && <AgentsSettings />}
|
||||
{activeTab === 'skills' && <SkillSettings />}
|
||||
{activeTab === 'memory' && <MemorySettings />}
|
||||
{activeTab === 'plugins' && <PluginSettings />}
|
||||
{activeTab === 'computerUse' && <ComputerUseSettings />}
|
||||
@ -4326,31 +4325,6 @@ function DetailStat({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ─── Skill Settings ──────────────────────────────────────
|
||||
|
||||
function SkillSettings() {
|
||||
const t = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-2xl">
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">
|
||||
{t('settings.skills.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-4">
|
||||
{t('settings.skills.redirectDescription')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => useTabStore.getState().openTab(SKILL_CENTER_TAB_ID, t('skillCenter.title'), 'skill-center')}
|
||||
className="inline-flex h-10 items-center gap-2 rounded-md bg-[var(--color-brand)] px-4 text-sm font-semibold text-[var(--color-on-primary)] transition-colors hover:bg-[var(--color-brand-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">auto_awesome</span>
|
||||
{t('settings.skills.openSkillCenter')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginSettings() {
|
||||
const selectedPlugin = usePluginStore((s) => s.selectedPlugin)
|
||||
const t = useTranslation()
|
||||
|
||||
@ -263,6 +263,20 @@ describe('SkillCenter', () => {
|
||||
expect(detailLayer.querySelector('.skill-market-detail-scrim')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('closes the marketplace detail drawer from the close icon', async () => {
|
||||
render(<SkillCenter />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'PPT Generator' }))
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: 'PPT Generator' })
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Close skill details' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('skill-market-detail-layer')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(useSkillMarketStore.getState().selectedDetail).toBeNull()
|
||||
expect(useSkillMarketStore.getState().isDetailLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('announces detail drawer skeleton loading without visible copy', async () => {
|
||||
mockedSkillMarketApi.detail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
|
||||
@ -28,6 +28,25 @@ describe('tabStore', () => {
|
||||
expect(useTabStore.getState().activeTabId).toBe('session-1')
|
||||
})
|
||||
|
||||
it('repairs an existing special tab type when opened through its canonical entrypoint', () => {
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Skills', type: 'skill-center', status: 'idle' }],
|
||||
activeTabId: SETTINGS_TAB_ID,
|
||||
})
|
||||
|
||||
useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings')
|
||||
|
||||
expect(useTabStore.getState().tabs).toEqual([
|
||||
{
|
||||
sessionId: SETTINGS_TAB_ID,
|
||||
title: 'Settings',
|
||||
type: 'settings',
|
||||
status: 'idle',
|
||||
},
|
||||
])
|
||||
expect(useTabStore.getState().activeTabId).toBe(SETTINGS_TAB_ID)
|
||||
})
|
||||
|
||||
it('stores a promoted terminal runtime id on new terminal tabs', () => {
|
||||
const tabId = useTabStore.getState().openTerminalTab('/tmp/project', '__session_terminal__session-1')
|
||||
|
||||
@ -133,4 +152,32 @@ describe('tabStore', () => {
|
||||
])
|
||||
expect(useTabStore.getState().activeTabId).toBe(SKILL_CENTER_TAB_ID)
|
||||
})
|
||||
|
||||
it('canonicalizes mismatched persisted special tab ids and types during restore', async () => {
|
||||
localStorage.setItem('cc-haha-open-tabs', JSON.stringify({
|
||||
openTabs: [
|
||||
{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'skill-center' },
|
||||
{ sessionId: SKILL_CENTER_TAB_ID, title: 'Skills', type: 'settings' },
|
||||
],
|
||||
activeTabId: SETTINGS_TAB_ID,
|
||||
}))
|
||||
|
||||
await useTabStore.getState().restoreTabs()
|
||||
|
||||
expect(useTabStore.getState().tabs).toEqual([
|
||||
{
|
||||
sessionId: SETTINGS_TAB_ID,
|
||||
title: 'Settings',
|
||||
type: 'settings',
|
||||
status: 'idle',
|
||||
},
|
||||
{
|
||||
sessionId: SKILL_CENTER_TAB_ID,
|
||||
title: 'Skills',
|
||||
type: 'skill-center',
|
||||
status: 'idle',
|
||||
},
|
||||
])
|
||||
expect(useTabStore.getState().activeTabId).toBe(SETTINGS_TAB_ID)
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,6 +15,7 @@ export const WORKBENCH_TAB_PREFIX = '__workbench__'
|
||||
export const SUBAGENT_TAB_PREFIX = '__subagent__'
|
||||
|
||||
export type TabType = 'session' | 'settings' | 'scheduled' | 'skill-center' | 'terminal' | 'trace' | 'traces' | 'workbench' | 'subagent'
|
||||
type PersistentSpecialTabType = 'settings' | 'scheduled' | 'skill-center' | 'traces'
|
||||
|
||||
export type Tab = {
|
||||
sessionId: string
|
||||
@ -55,11 +56,29 @@ type TabStore = {
|
||||
restoreTabs: () => Promise<void>
|
||||
}
|
||||
|
||||
const PERSISTENT_SPECIAL_TAB_IDS: Record<PersistentSpecialTabType, string> = {
|
||||
settings: SETTINGS_TAB_ID,
|
||||
scheduled: SCHEDULED_TAB_ID,
|
||||
'skill-center': SKILL_CENTER_TAB_ID,
|
||||
traces: TRACE_LIST_TAB_ID,
|
||||
}
|
||||
|
||||
function getPersistentSpecialTabType(tab: Pick<Tab, 'sessionId'> & { type?: TabType }): PersistentSpecialTabType | null {
|
||||
if (tab.sessionId === SETTINGS_TAB_ID) return 'settings'
|
||||
if (tab.sessionId === SCHEDULED_TAB_ID) return 'scheduled'
|
||||
if (tab.sessionId === SKILL_CENTER_TAB_ID) return 'skill-center'
|
||||
if (tab.sessionId === TRACE_LIST_TAB_ID) return 'traces'
|
||||
if (tab.type === 'settings' || tab.type === 'scheduled' || tab.type === 'skill-center' || tab.type === 'traces') {
|
||||
return tab.type
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const useTabStore = create<TabStore>((set, get) => ({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
|
||||
openTab: (sessionId, title, type = 'session') => {
|
||||
openTab: (sessionId, title, type) => {
|
||||
const { tabs } = get()
|
||||
const existing = tabs.find((t) => t.sessionId === sessionId)
|
||||
if (existing) {
|
||||
@ -69,7 +88,7 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
? {
|
||||
...tab,
|
||||
title,
|
||||
...(!(tab as Partial<Tab>).type ? { type } : {}),
|
||||
type: type ?? tab.type ?? 'session',
|
||||
}
|
||||
: tab,
|
||||
),
|
||||
@ -77,7 +96,7 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
})
|
||||
} else {
|
||||
set({
|
||||
tabs: [...tabs, { sessionId, title, type, status: 'idle' }],
|
||||
tabs: [...tabs, { sessionId, title, type: type ?? 'session', status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
}
|
||||
@ -311,15 +330,16 @@ export const useTabStore = create<TabStore>((set, get) => ({
|
||||
const validTabs: Tab[] = data.openTabs
|
||||
.filter((t) => {
|
||||
// Special tabs are always valid
|
||||
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'skill-center' || t.type === 'traces') return true
|
||||
if (getPersistentSpecialTabType(t)) return true
|
||||
if (t.type === 'trace') return !!t.traceSessionId && existingIds.has(t.traceSessionId)
|
||||
if (t.type === 'terminal') return false
|
||||
// Session tabs must exist on server
|
||||
return existingIds.has(t.sessionId)
|
||||
})
|
||||
.map((t) => {
|
||||
if (t.type === 'settings' || t.type === 'scheduled' || t.type === 'skill-center' || t.type === 'traces') {
|
||||
return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
|
||||
const specialType = getPersistentSpecialTabType(t)
|
||||
if (specialType) {
|
||||
return { sessionId: PERSISTENT_SPECIAL_TAB_IDS[specialType], title: t.title, type: specialType, status: 'idle' as const }
|
||||
}
|
||||
if (t.type === 'trace' && t.traceSessionId) {
|
||||
const sourceTitle = sessions.find((s) => s.id === t.traceSessionId)?.title || t.title
|
||||
|
||||
@ -71,4 +71,12 @@ describe('uiStore settings tab persistence', () => {
|
||||
|
||||
expect(useUIStore.getState().activeSettingsTab).toBe('providers')
|
||||
})
|
||||
|
||||
it('normalizes the removed Skills settings tab when hydrating persisted state', async () => {
|
||||
window.localStorage.setItem('cc-haha-active-settings-tab', 'skills')
|
||||
|
||||
const { useUIStore } = await import('./uiStore')
|
||||
|
||||
expect(useUIStore.getState().activeSettingsTab).toBe('general')
|
||||
})
|
||||
})
|
||||
|
||||
@ -37,6 +37,7 @@ function isSettingsTab(value: unknown): value is SettingsTab {
|
||||
function getStoredSettingsTab(): SettingsTab {
|
||||
try {
|
||||
const stored = localStorage.getItem(ACTIVE_SETTINGS_TAB_STORAGE_KEY)
|
||||
if (stored === 'skills') return 'general'
|
||||
if (isSettingsTab(stored)) return stored
|
||||
} catch { /* localStorage unavailable */ }
|
||||
return 'providers'
|
||||
@ -134,8 +135,9 @@ export const useUIStore = create<UIStore>((set) => ({
|
||||
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
||||
setActiveView: (view) => set({ activeView: view }),
|
||||
setActiveSettingsTab: (tab) => {
|
||||
try { localStorage.setItem(ACTIVE_SETTINGS_TAB_STORAGE_KEY, tab) } catch { /* noop */ }
|
||||
set({ activeSettingsTab: tab })
|
||||
const next = tab === 'skills' ? 'general' : tab
|
||||
try { localStorage.setItem(ACTIVE_SETTINGS_TAB_STORAGE_KEY, next) } catch { /* noop */ }
|
||||
set({ activeSettingsTab: next })
|
||||
},
|
||||
setPendingSettingsTab: (tab) => set({ pendingSettingsTab: tab }),
|
||||
setPendingMemoryPath: (path) => set({ pendingMemoryPath: path }),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user