From 1cde0f338570e89edc74bcf9989eb71c07419ede Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sat, 15 Aug 2026 20:57:46 +0800 Subject: [PATCH] feat: show active conversations in sidebar --- src/renderer/src/App.test.tsx | 32 +++++++++++++++++++++ src/renderer/src/App.tsx | 33 ++++++++++++++++++++++ src/renderer/src/i18n/locales/en-US/app.ts | 1 + src/renderer/src/i18n/locales/zh-CN/app.ts | 1 + src/renderer/src/styles.css | 13 +++++++++ 5 files changed, 80 insertions(+) diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index ba5dc02..f1597ed 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -1592,6 +1592,20 @@ describe('App', () => { if (!request) { throw new Error('Missing request') } + const conversationList = + container.querySelector('.conversation-list') + if (!conversationList) { + throw new Error('Missing conversation list') + } + const activeIndicator = within(conversationList).getByRole('status', { + name: '会话正在活动' + }) + expect(activeIndicator).toHaveClass( + 'conversation-activity-indicator' + ) + expect(activeIndicator.closest('.conversation-row')).toHaveTextContent( + '离开页面后继续生成' + ) await act( () => new Promise((resolve) => @@ -1613,6 +1627,11 @@ describe('App', () => { fireEvent.click(screen.getByRole('button', { name: '知识库' })) expect(chat).toBeInTheDocument() expect(chat.closest('[data-route="chat"]')).toHaveAttribute('hidden') + expect( + within(conversationList).getByRole('status', { + name: '会话正在活动' + }) + ).toBeInTheDocument() act(() => { agentListener?.({ requestId: request.requestId, @@ -1620,6 +1639,19 @@ describe('App', () => { delta: '后台新增的回复内容' }) }) + act(() => { + agentListener?.({ + requestId: request.requestId, + type: 'done' + }) + }) + await waitFor(() => + expect( + within(conversationList).queryByRole('status', { + name: '会话正在活动' + }) + ).not.toBeInTheDocument() + ) fireEvent.click(screen.getByRole('button', { name: '对话' })) expect(await screen.findByText('后台新增的回复内容')).toBeInTheDocument() diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4749071..3ea4913 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2046,6 +2046,26 @@ function App(): React.JSX.Element { const activityRecordsRef = useRef(activityRecords) const activeRuns = useRef(new Map()) const preparingConversations = useRef(new Set()) + const [activeConversationIds, setActiveConversationIds] = useState< + ReadonlySet + >(() => new Set()) + const setConversationActivity = useCallback( + (conversationId: string, active: boolean): void => { + setActiveConversationIds((current) => { + if (current.has(conversationId) === active) { + return current + } + const next = new Set(current) + if (active) { + next.add(conversationId) + } else { + next.delete(conversationId) + } + return next + }) + }, + [] + ) const hydratingArtifactIds = useRef(new Set()) const knowledgeScopeInitialized = useRef(false) const inputRef = useRef(null) @@ -3464,6 +3484,7 @@ function App(): React.JSX.Element { } }) activeRuns.current.delete(event.requestId) + setConversationActivity(run.conversationId, false) flushConversationPersistenceAfterRenderRef.current = true } }, @@ -3471,6 +3492,7 @@ function App(): React.JSX.Element { recordActivity, loadWorkspaceChanges, refreshTokenUsage, + setConversationActivity, updateMessage, updateRequestActivity ] @@ -4651,6 +4673,7 @@ function App(): React.JSX.Element { delete next[conversationId] return next }) + setConversationActivity(conversationId, false) const remaining = conversations.filter( (conversation) => conversation.id !== conversationId ) @@ -4954,6 +4977,7 @@ function App(): React.JSX.Element { runtime.capability === 'image-generation' ? '' : selectedExpertId const workModeSnapshot = effectiveWorkMode preparingConversations.current.add(conversationId) + setConversationActivity(conversationId, true) setInput('') updateAttachments([]) const userMessage: Message = { @@ -5546,6 +5570,7 @@ function App(): React.JSX.Element { await window.goodbuddy.agent.cancel(requestId) } activeRuns.current.clear() + setActiveConversationIds(new Set()) for (const attachments of attachmentsRef.current.values()) { for (const attachment of attachments) { await window.goodbuddy.context.remove(attachment.id) @@ -5889,6 +5914,14 @@ function App(): React.JSX.Element { {formatTime(conversation.updatedAt, locale)} + {activeConversationIds.has(conversation.id) && ( + + )}