mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
fix(desktop): protect unloaded sessions from tab cleanup #1217
This commit is contained in:
@@ -2167,6 +2167,122 @@ describe('TabBar', () => {
|
||||
expect(useTabStore.getState().activeTabId).toBe('tab-2')
|
||||
})
|
||||
|
||||
it('does not delete a session when its list and history failed to load', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
const { useSessionStore } = await import('../../stores/sessionStore')
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [
|
||||
{ sessionId: 'recovered-session', title: 'Recovered Session', type: 'session', status: 'idle' },
|
||||
],
|
||||
activeTabId: 'recovered-session',
|
||||
})
|
||||
useSessionStore.setState({
|
||||
sessions: [],
|
||||
isLoading: false,
|
||||
error: 'Session list request timed out',
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
disconnectSession: vi.fn(),
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Close Recovered Session'))
|
||||
|
||||
expect(sessionsApiMock.delete).not.toHaveBeenCalled()
|
||||
expect(useTabStore.getState().tabs).toEqual([])
|
||||
})
|
||||
|
||||
it('does not delete a session whose server metadata reports messages', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
const { useSessionStore } = await import('../../stores/sessionStore')
|
||||
const sessionId = 'persisted-session'
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId, title: 'New Session', type: 'session', status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
useSessionStore.setState({
|
||||
sessions: [{
|
||||
id: sessionId,
|
||||
title: 'New Session',
|
||||
createdAt: '2026-08-13T00:00:00.000Z',
|
||||
modifiedAt: '2026-08-13T00:00:00.000Z',
|
||||
messageCount: 2,
|
||||
projectPath: '/repo',
|
||||
workDir: '/repo',
|
||||
workDirExists: true,
|
||||
}],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[sessionId]: { ...makeChatSession('idle'), historyStatus: 'ready' },
|
||||
},
|
||||
disconnectSession: vi.fn(),
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Close New Session'))
|
||||
|
||||
expect(sessionsApiMock.delete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deletes a confirmed empty placeholder session when closing its tab', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
const { useSessionStore } = await import('../../stores/sessionStore')
|
||||
const sessionId = 'empty-session'
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId, title: 'New Session', type: 'session', status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
useSessionStore.setState({
|
||||
sessions: [{
|
||||
id: sessionId,
|
||||
title: 'New Session',
|
||||
createdAt: '2026-08-13T00:00:00.000Z',
|
||||
modifiedAt: '2026-08-13T00:00:00.000Z',
|
||||
messageCount: 0,
|
||||
projectPath: '/repo',
|
||||
workDir: '/repo',
|
||||
workDirExists: true,
|
||||
}],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[sessionId]: { ...makeChatSession('idle'), historyStatus: 'ready' },
|
||||
},
|
||||
disconnectSession: vi.fn(),
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Close New Session'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionsApiMock.delete).toHaveBeenCalledWith(sessionId)
|
||||
})
|
||||
})
|
||||
|
||||
it('closes terminal tabs without disconnecting chat sessions', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
|
||||
@@ -367,10 +367,17 @@ export function TabBar() {
|
||||
useChatStore.getState().stopGeneration(tab.sessionId)
|
||||
}
|
||||
if (!isRunning || stopRunning) {
|
||||
// Auto-delete empty sessions (placeholder title, no messages sent)
|
||||
// Auto-delete only when both server metadata and the loaded transcript
|
||||
// confirm this is an empty placeholder. Missing state can mean a timed-out
|
||||
// recovery load, so treating it as empty risks deleting a real session.
|
||||
const sessionEntry = useSessionStore.getState().sessions.find((s) => s.id === tab.sessionId)
|
||||
const chatEntry = useChatStore.getState().sessions[tab.sessionId]
|
||||
if (isPlaceholderSessionTitle(sessionEntry?.title) && (!chatEntry || chatEntry.messages.length === 0)) {
|
||||
if (
|
||||
sessionEntry?.messageCount === 0 &&
|
||||
isPlaceholderSessionTitle(sessionEntry.title) &&
|
||||
chatEntry?.historyStatus === 'ready' &&
|
||||
chatEntry.messages.length === 0
|
||||
) {
|
||||
void useSessionStore.getState().deleteSession(tab.sessionId)
|
||||
}
|
||||
disconnectSession(tab.sessionId)
|
||||
|
||||
Reference in New Issue
Block a user