mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
fix(desktop): preserve chat view across tab switches
This commit is contained in:
@@ -522,6 +522,27 @@ describe('ChatInput file mentions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a retained conversation draft when settings becomes the selected tab', () => {
|
||||
useTabStore.setState((state) => ({
|
||||
tabs: [...state.tabs, { sessionId: '__settings__', title: 'Settings', type: 'settings', status: 'idle' }],
|
||||
}))
|
||||
const { rerender } = render(<ChatInput sessionId={sessionId} />)
|
||||
setComposerText('keep this draft', 15)
|
||||
act(() => getComposerElement().focus())
|
||||
expect(getComposerElement()).toHaveFocus()
|
||||
|
||||
act(() => useTabStore.getState().setActiveTab('__settings__'))
|
||||
rerender(<ChatInput sessionId={sessionId} visible={false} />)
|
||||
|
||||
expect(getComposerText()).toBe('keep this draft')
|
||||
expect(getComposerElement()).not.toHaveFocus()
|
||||
expect(useChatStore.getState().sessions['__settings__']).toBeUndefined()
|
||||
|
||||
act(() => useTabStore.getState().setActiveTab(sessionId))
|
||||
rerender(<ChatInput sessionId={sessionId} visible />)
|
||||
expect(getComposerText()).toBe('keep this draft')
|
||||
})
|
||||
|
||||
it('keeps the unsent draft when switching project on an empty active session', async () => {
|
||||
installElectronFileHost()
|
||||
mocks.dialogOpen.mockResolvedValueOnce('/other')
|
||||
|
||||
@@ -79,6 +79,8 @@ type Attachment = ComposerAttachment
|
||||
type ChatInputProps = {
|
||||
variant?: 'default' | 'hero'
|
||||
compact?: boolean
|
||||
sessionId?: string
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
const EMPTY_COMPOSER_REFERENCES: ComposerReferenceCandidate[] = []
|
||||
@@ -136,7 +138,7 @@ function insertComposerTokenAtRange(value: string, start: number, end: number, t
|
||||
}
|
||||
}
|
||||
|
||||
export function ChatInput({ variant = 'default', compact = false }: ChatInputProps) {
|
||||
export function ChatInput({ variant = 'default', compact = false, sessionId, visible = true }: ChatInputProps) {
|
||||
const t = useTranslation()
|
||||
const isMobileComposer = useMobileViewport() && !isDesktopRuntime()
|
||||
// The shell, not the panel inside it: the panel's own `max-w` changes with
|
||||
@@ -206,9 +208,10 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
sendQueuedUserMessage,
|
||||
setPreparingTurn,
|
||||
} = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const selectedTabId = useTabStore((s) => s.activeTabId)
|
||||
const activeTabId = sessionId ?? selectedTabId
|
||||
const activeTabType = useTabStore((s) =>
|
||||
s.tabs.find((tab) => tab.sessionId === s.activeTabId)?.type,
|
||||
s.tabs.find((tab) => tab.sessionId === (sessionId ?? s.activeTabId))?.type,
|
||||
)
|
||||
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
||||
const repositoryLaunchDraft = sessionState?.repositoryLaunchDraft
|
||||
@@ -408,8 +411,14 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
}, [mentions])
|
||||
|
||||
useEffect(() => {
|
||||
composerRef.current?.focus()
|
||||
}, [isActive])
|
||||
if (visible) composerRef.current?.focus()
|
||||
}, [isActive, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) return
|
||||
const focused = document.activeElement
|
||||
if (focused instanceof HTMLElement && panelRef.current?.contains(focused)) focused.blur()
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!composerPrefill || !activeTabId) return
|
||||
@@ -439,7 +448,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
setAtCursorPos(-1)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
composerRef.current?.focus()
|
||||
if (visible) composerRef.current?.focus()
|
||||
if (composerPrefill.mode !== 'append') {
|
||||
composerRef.current?.setSelectionOffsets(composerPrefill.text.length)
|
||||
}
|
||||
@@ -451,6 +460,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
composerPrefill,
|
||||
setComposerAttachments,
|
||||
setComposerInput,
|
||||
visible,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -473,7 +483,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
clearComposerInsertion(activeTabId, composerInsertion.nonce)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
composerRef.current?.focus()
|
||||
if (visible) composerRef.current?.focus()
|
||||
composerRef.current?.setSelectionOffsets(next.cursorPos)
|
||||
})
|
||||
}, [
|
||||
@@ -481,6 +491,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
addWorkspaceReference,
|
||||
clearComposerInsertion,
|
||||
composerInsertion,
|
||||
visible,
|
||||
isMemberSession,
|
||||
setComposerInput,
|
||||
])
|
||||
|
||||
@@ -300,6 +300,29 @@ describe('ContentRouter tab surfaces', () => {
|
||||
expect(screen.getByTestId('settings-page')).toBeInTheDocument()
|
||||
expect(useTabStore.getState().tabs.find(tab => tab.sessionId === 'session-1')).toMatchObject({ type: 'session' })
|
||||
})
|
||||
|
||||
it('keeps the current conversation mounted while settings is open', () => {
|
||||
useTabStore.setState({
|
||||
tabs: [
|
||||
{ sessionId: 'session-1', title: 'Chat', type: 'session', status: 'idle' },
|
||||
{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' },
|
||||
],
|
||||
activeTabId: 'session-1',
|
||||
})
|
||||
|
||||
render(<ContentRouter />)
|
||||
const conversation = screen.getByTestId('active-session')
|
||||
|
||||
act(() => useTabStore.getState().setActiveTab(SETTINGS_TAB_ID))
|
||||
expect(screen.getByTestId('settings-page')).toBeInTheDocument()
|
||||
expect(conversation).toBeInTheDocument()
|
||||
expect(conversation.closest('[aria-hidden]')).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(conversation.closest('[aria-hidden]')).toHaveAttribute('inert')
|
||||
|
||||
act(() => useTabStore.getState().setActiveTab('session-1'))
|
||||
expect(screen.getByTestId('active-session')).toBe(conversation)
|
||||
expect(screen.queryByTestId('settings-page')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('routes the independent connectors tab', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, type ReactNode } from 'react'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { EmptySession } from '../../pages/EmptySession'
|
||||
import { ActiveSession } from '../../pages/ActiveSession'
|
||||
@@ -17,6 +17,18 @@ export function ContentRouter() {
|
||||
const tabs = useTabStore((s) => s.tabs)
|
||||
const activeTabType = tabs.find((t) => t.sessionId === activeTabId)?.type
|
||||
const terminalTabs = tabs.filter((tab) => tab.type === 'terminal')
|
||||
const activeSessionId = activeTabType === 'session' ? activeTabId : null
|
||||
const [lastSessionId, setLastSessionId] = useState<string | null>(activeSessionId)
|
||||
const retainedSessionId = activeSessionId ?? (
|
||||
tabs.some((tab) => tab.sessionId === lastSessionId && tab.type === 'session')
|
||||
? lastSessionId
|
||||
: null
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSessionId) setLastSessionId(activeSessionId)
|
||||
else if (lastSessionId && !retainedSessionId) setLastSessionId(null)
|
||||
}, [activeSessionId, lastSessionId, retainedSessionId])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabType !== 'workbench') return
|
||||
@@ -79,12 +91,24 @@ export function ContentRouter() {
|
||||
page = teamTab?.teamLeadSessionId
|
||||
? <AgentTeamsWorkbenchTab tabId={activeTabId} leadSessionId={teamTab.teamLeadSessionId} />
|
||||
: <EmptySession />
|
||||
} else if (activeTabType !== 'terminal') {
|
||||
page = <ActiveSession />
|
||||
} else if (activeTabType !== 'terminal' && activeTabType !== 'session') {
|
||||
page = <EmptySession />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||||
{retainedSessionId && (
|
||||
<div
|
||||
aria-hidden={!activeSessionId}
|
||||
{...(activeSessionId ? {} : { inert: '' })}
|
||||
data-testid="session-tab-panel"
|
||||
className={`absolute inset-0 flex min-h-0 flex-col overflow-hidden ${
|
||||
activeSessionId ? 'z-10 opacity-100' : 'pointer-events-none z-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<ActiveSession sessionId={retainedSessionId} active={Boolean(activeSessionId)} />
|
||||
</div>
|
||||
)}
|
||||
{page && (
|
||||
<div className="absolute inset-0 z-10 flex min-h-0 flex-col overflow-hidden">
|
||||
{page}
|
||||
|
||||
@@ -37,14 +37,14 @@ vi.mock('../hooks/useMobileViewport', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../components/chat/MessageList', () => ({
|
||||
MessageList: ({ compact }: { compact?: boolean }) => (
|
||||
<div data-testid="message-list" data-compact={compact ? 'true' : 'false'} />
|
||||
MessageList: ({ compact, sessionId }: { compact?: boolean; sessionId?: string }) => (
|
||||
<div data-testid="message-list" data-compact={compact ? 'true' : 'false'} data-session-id={sessionId} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/chat/ChatInput', () => ({
|
||||
ChatInput: ({ compact, variant }: { compact?: boolean; variant?: string }) => (
|
||||
<div data-testid="chat-input" data-compact={compact ? 'true' : 'false'} data-variant={variant} />
|
||||
ChatInput: ({ compact, variant, sessionId, visible }: { compact?: boolean; variant?: string; sessionId?: string; visible?: boolean }) => (
|
||||
<div data-testid="chat-input" data-compact={compact ? 'true' : 'false'} data-variant={variant} data-session-id={sessionId} data-visible={visible ? 'true' : 'false'} />
|
||||
),
|
||||
}))
|
||||
|
||||
@@ -141,6 +141,55 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('ActiveSession task polling', () => {
|
||||
it('keeps the same message list bound to its session while settings is selected', () => {
|
||||
const sessionId = 'retained-session'
|
||||
useSessionStore.setState({
|
||||
sessions: [{
|
||||
id: sessionId,
|
||||
title: 'Retained Session',
|
||||
createdAt: '2026-05-07T00:00:00.000Z',
|
||||
modifiedAt: '2026-05-07T00:00:00.000Z',
|
||||
messageCount: 1,
|
||||
projectPath: '/workspace/project',
|
||||
workDir: '/workspace/project',
|
||||
workDirExists: true,
|
||||
}],
|
||||
activeSessionId: sessionId,
|
||||
})
|
||||
useTabStore.setState({
|
||||
tabs: [
|
||||
{ sessionId, title: 'Retained Session', type: 'session', status: 'idle' },
|
||||
{ sessionId: '__settings__', title: 'Settings', type: 'settings', status: 'idle' },
|
||||
],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[sessionId]: {
|
||||
...createDefaultSessionState(),
|
||||
connectionState: 'connected',
|
||||
messages: [{ id: 'existing', type: 'assistant_text', content: 'ready', timestamp: 1 }],
|
||||
historyStatus: 'ready',
|
||||
historyHydrated: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { rerender } = render(<ActiveSession sessionId={sessionId} active />)
|
||||
const messageList = screen.getByTestId('message-list')
|
||||
const chatInput = screen.getByTestId('chat-input')
|
||||
|
||||
act(() => useTabStore.getState().setActiveTab('__settings__'))
|
||||
rerender(<ActiveSession sessionId={sessionId} active={false} />)
|
||||
|
||||
expect(screen.getByTestId('message-list')).toBe(messageList)
|
||||
expect(screen.getByTestId('chat-input')).toBe(chatInput)
|
||||
expect(messageList).toHaveAttribute('data-session-id', sessionId)
|
||||
expect(chatInput).toHaveAttribute('data-session-id', sessionId)
|
||||
expect(chatInput).toHaveAttribute('data-visible', 'false')
|
||||
expect(useChatStore.getState().sessions['__settings__']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shows cleaned worktrees as retained history and uses the source project for tools', () => {
|
||||
const sessionId = 'cleaned-worktree-session'
|
||||
useSettingsStore.setState({ locale: 'en' })
|
||||
|
||||
@@ -316,11 +316,12 @@ function TerminalResizeHandle() {
|
||||
)
|
||||
}
|
||||
|
||||
export function ActiveSession() {
|
||||
export function ActiveSession({ sessionId, active = true }: { sessionId?: string; active?: boolean } = {}) {
|
||||
const isMobileLayout = useMobileViewport() && !isDesktopRuntime()
|
||||
const workbenchPanelRef = useRef<HTMLElement>(null)
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const activeTabType = useTabStore((s) => s.tabs.find((tab) => tab.sessionId === s.activeTabId)?.type ?? null)
|
||||
const selectedTabId = useTabStore((s) => s.activeTabId)
|
||||
const activeTabId = sessionId ?? selectedTabId
|
||||
const activeTabType = useTabStore((s) => s.tabs.find((tab) => tab.sessionId === (sessionId ?? s.activeTabId))?.type ?? null)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const connectToSession = useChatStore((s) => s.connectToSession)
|
||||
const stopBackgroundTask = useChatStore((s) => s.stopBackgroundTask)
|
||||
@@ -408,12 +409,12 @@ export function ActiveSession() {
|
||||
|
||||
// Subscribed once for the app, not per task: the owner of each event is
|
||||
// resolved from the page id, so a background task's pages keep reporting.
|
||||
useWorkspaceBrowserEventBridge(!isMobileLayout)
|
||||
useWorkspaceFocusReturn(workspaceEnabled ? activeTabId : null)
|
||||
useWorkspaceBrowserEventBridge(active && !isMobileLayout)
|
||||
useWorkspaceFocusReturn(active && workspaceEnabled ? activeTabId : null)
|
||||
useWorkspaceShortcuts({
|
||||
sessionId: activeTabId,
|
||||
cwd: getSessionTerminalCwd(session) ?? '',
|
||||
enabled: workspaceEnabled,
|
||||
enabled: active && workspaceEnabled,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -877,7 +878,7 @@ export function ActiveSession() {
|
||||
{historyError}
|
||||
</div>
|
||||
) : (
|
||||
<MessageList compact={showRightPanel} mobileLayout={isMobileLayout} />
|
||||
<MessageList sessionId={activeTabId ?? undefined} compact={showRightPanel} mobileLayout={isMobileLayout} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -897,6 +898,8 @@ export function ActiveSession() {
|
||||
) : null}
|
||||
|
||||
<ChatInput
|
||||
sessionId={activeTabId ?? undefined}
|
||||
visible={active}
|
||||
variant={isEmpty && !showRightPanel ? 'hero' : 'default'}
|
||||
compact={showRightPanel}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AgentTaskNotification, UIMessage } from '../types/chat'
|
||||
import type { MessageEntry, SessionListItem } from '../types/session'
|
||||
import type { SessionHistoryPage } from '../api/sessions'
|
||||
import type { SavedProvider } from '../types/provider'
|
||||
import type { Tab } from './tabStore'
|
||||
import {
|
||||
buildMainSessionActivityModel,
|
||||
buildSessionActivityModel,
|
||||
@@ -36,6 +37,7 @@ const {
|
||||
connectionStateHandlers,
|
||||
sendSubagentMessageMock,
|
||||
tabStoreSnapshot,
|
||||
tabStoreListeners,
|
||||
providerStoreSnapshot,
|
||||
} = vi.hoisted(() => ({
|
||||
sendMock: vi.fn(),
|
||||
@@ -76,7 +78,8 @@ const {
|
||||
},
|
||||
connectionStateHandlers: new Map<string, (state: 'connecting' | 'connected' | 'reconnecting' | 'disconnected') => void>(),
|
||||
sendSubagentMessageMock: vi.fn(async () => ({ ok: true })),
|
||||
tabStoreSnapshot: { tabs: [] as Array<Record<string, unknown>> },
|
||||
tabStoreSnapshot: { tabs: [] as Tab[], activeTabId: null as string | null },
|
||||
tabStoreListeners: new Set<(state: any, previous: any) => void>(),
|
||||
providerStoreSnapshot: { providers: [] as SavedProvider[], activeId: null as string | null },
|
||||
}))
|
||||
|
||||
@@ -136,9 +139,14 @@ vi.mock('./tabStore', () => ({
|
||||
useTabStore: {
|
||||
getState: () => ({
|
||||
tabs: tabStoreSnapshot.tabs,
|
||||
activeTabId: tabStoreSnapshot.activeTabId,
|
||||
updateTabStatus: updateTabStatusMock,
|
||||
updateTabTitle: updateTabTitleMock,
|
||||
}),
|
||||
subscribe: (listener: (state: any, previous: any) => void) => {
|
||||
tabStoreListeners.add(listener)
|
||||
return () => tabStoreListeners.delete(listener)
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -14792,7 +14800,9 @@ describe('chatStore history mapping', () => {
|
||||
const subagentSessionId = '__subagent__parent-session__tool-agent-1'
|
||||
tabStoreSnapshot.tabs = [{
|
||||
sessionId: subagentSessionId,
|
||||
title: 'Subagent',
|
||||
type: 'subagent',
|
||||
status: 'idle',
|
||||
sourceSessionId: 'parent-session',
|
||||
subagentToolUseId: 'tool-agent-1',
|
||||
subagentTaskId: 'agent-1',
|
||||
@@ -15873,9 +15883,11 @@ describe('chatStore inactive complete-page retention', () => {
|
||||
const { readFileSync } = await import('node:fs')
|
||||
imageData = `data:image/png;base64,${readFileSync('src/assets/pets/action-sheet-guide.zh.png').toString('base64')}`
|
||||
const { useTabStore } = await import('./tabStore')
|
||||
tabStoreSnapshot.tabs = []
|
||||
tabStoreSnapshot.activeTabId = null
|
||||
const tabState = useTabStore.getState()
|
||||
activeSessionId = 'image-cache-0'
|
||||
const spy = vi.spyOn(useTabStore, 'getState').mockImplementation(() => ({ ...tabState, activeTabId: activeSessionId }))
|
||||
const spy = vi.spyOn(useTabStore, 'getState').mockImplementation(() => ({ ...tabState, tabs: tabStoreSnapshot.tabs, activeTabId: activeSessionId }))
|
||||
restoreTabState = () => spy.mockRestore()
|
||||
getMemberBySessionIdMock.mockReturnValue(null)
|
||||
vi.mocked(sessionsApi.getFullHistory).mockReset()
|
||||
@@ -15885,9 +15897,47 @@ describe('chatStore inactive complete-page retention', () => {
|
||||
|
||||
afterEach(() => {
|
||||
restoreTabState()
|
||||
tabStoreSnapshot.tabs = []
|
||||
tabStoreSnapshot.activeTabId = null
|
||||
useChatStore.setState({ ...initialState, sessions: {} })
|
||||
})
|
||||
|
||||
it('keeps the last open chat history when a settings or other page takes focus', () => {
|
||||
const sessions = imageSessions()
|
||||
const recentId = 'image-cache-4'
|
||||
for (const [id, session] of Object.entries(sessions)) {
|
||||
useChatStore.getState().markHistoryRowsDurable(id, session.messages)
|
||||
}
|
||||
const sessionTab: Tab = { sessionId: recentId, type: 'session', title: 'Recent', status: 'idle' }
|
||||
const settingsTab: Tab = { sessionId: '__settings__', type: 'settings', title: 'Settings', status: 'idle' }
|
||||
const marketTab: Tab = { sessionId: '__market__', type: 'market', title: 'Market', status: 'idle' }
|
||||
const previous = { tabs: [sessionTab], activeTabId: recentId }
|
||||
const settings = { tabs: [sessionTab, settingsTab], activeTabId: settingsTab.sessionId }
|
||||
tabStoreSnapshot.tabs = settings.tabs
|
||||
activeSessionId = settings.activeTabId
|
||||
for (const listener of tabStoreListeners) listener(settings, previous)
|
||||
|
||||
useChatStore.getState().applyBoundedUpdate(() => ({ sessions }))
|
||||
expect(useChatStore.getState().sessions[recentId]!.messages).toHaveLength(1)
|
||||
expect(useChatStore.getState().sessions['image-cache-0']!.messages).toHaveLength(0)
|
||||
|
||||
const market = { tabs: [...settings.tabs, marketTab], activeTabId: marketTab.sessionId }
|
||||
tabStoreSnapshot.tabs = market.tabs
|
||||
activeSessionId = market.activeTabId
|
||||
for (const listener of tabStoreListeners) listener(market, settings)
|
||||
useChatStore.getState().applyBoundedUpdate(state => ({ sessions: { ...state.sessions } }))
|
||||
expect(useChatStore.getState().sessions[recentId]!.messages).toHaveLength(1)
|
||||
|
||||
const closed = { tabs: [settingsTab, marketTab], activeTabId: marketTab.sessionId }
|
||||
tabStoreSnapshot.tabs = closed.tabs
|
||||
for (const listener of tabStoreListeners) listener(closed, market)
|
||||
for (const [id, session] of Object.entries(sessions)) {
|
||||
useChatStore.getState().markHistoryRowsDurable(id, session.messages)
|
||||
}
|
||||
useChatStore.getState().applyBoundedUpdate(() => ({ sessions }))
|
||||
expect(useChatStore.getState().sessions[recentId]!.messages).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps live rows intact while an idle-tab eviction would drop durable pages', () => {
|
||||
// Live rows that have not yet been persisted are not eligible for the
|
||||
// durable eviction set, so a tab holding them stays mounted whole.
|
||||
|
||||
@@ -2838,6 +2838,12 @@ function shouldPrewarmSession(sessionId: string): boolean {
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatStore>((setState, get) => {
|
||||
let lastViewedSessionId: string | null = null
|
||||
useTabStore.subscribe((tabState, previousTabState) => {
|
||||
if (tabState.activeTabId === previousTabState.activeTabId) return
|
||||
const previousTab = previousTabState.tabs.find(tab => tab.sessionId === previousTabState.activeTabId)
|
||||
if (previousTab?.type === 'session') lastViewedSessionId = previousTab.sessionId
|
||||
})
|
||||
const set = (update: Partial<ChatStore> | ((state: ChatStore) => Partial<ChatStore>)) => {
|
||||
setState((previous) => {
|
||||
const patch = typeof update === 'function' ? update(previous) : update
|
||||
@@ -2863,6 +2869,11 @@ export const useChatStore = create<ChatStore>((setState, get) => {
|
||||
const tabState = useTabStore.getState()
|
||||
const activeTab = tabState.tabs.find(tab => tab.sessionId === tabState.activeTabId)
|
||||
const activeIds = new Set([tabState.activeTabId, activeTab?.sourceSessionId, activeTab?.workbenchSessionId, activeTab?.teamLeadSessionId])
|
||||
// Keep the chat the user just left warm while a non-chat page is open.
|
||||
// Other idle tabs remain eligible, and closing this tab releases it.
|
||||
if (activeTab?.type !== 'session' && tabState.tabs.some(tab => tab.type === 'session' && tab.sessionId === lastViewedSessionId)) {
|
||||
activeIds.add(lastViewedSessionId)
|
||||
}
|
||||
const cacheSizes = new Map(Object.entries(sessions).map(([id, session]) => [id, historyCacheBytes(session)]))
|
||||
let retainedBytes = [...cacheSizes.values()].reduce((total, bytes) => total + bytes, 0)
|
||||
if (retainedBytes > CHAT_HISTORY_CACHE_BYTES) {
|
||||
|
||||
Reference in New Issue
Block a user