From e9b328d65b721e4d087648f4c6e9b5fc29d15ab4 Mon Sep 17 00:00:00 2001
From: zhb <50901800+zhbdesign@users.noreply.github.com>
Date: Fri, 10 Jul 2026 09:24:52 +0800
Subject: [PATCH] =?UTF-8?q?Sidebar=E4=BB=8E=E8=AE=A2=E9=98=85=20sessions?=
=?UTF-8?q?=EF=BC=88=E6=AF=8F=E6=AC=A1=20token=20flush=20=E6=96=B0?=
=?UTF-8?q?=E5=BC=95=E7=94=A8=EF=BC=89=E6=94=B9=E4=B8=BA=E8=AE=A2=E9=98=85?=
=?UTF-8?q?=20runningSession?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Sidebar从订阅 sessions(每次 token flush 新引用)改为订阅 runningSession
---
.../src/components/layout/Sidebar.test.tsx | 192 ++++++++++++++++++
1 file changed, 192 insertions(+)
diff --git a/desktop/src/components/layout/Sidebar.test.tsx b/desktop/src/components/layout/Sidebar.test.tsx
index 99465d3a..e1abd1e0 100644
--- a/desktop/src/components/layout/Sidebar.test.tsx
+++ b/desktop/src/components/layout/Sidebar.test.tsx
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import '@testing-library/jest-dom'
+import { Profiler, type ProfilerOnRenderCallback } from 'react'
const desktopUiPreferencesApiMock = vi.hoisted(() => ({
getPreferences: vi.fn(),
@@ -1295,4 +1296,195 @@ describe('Sidebar', () => {
value: originalVisibility,
})
})
+
+ describe('streaming optimization', () => {
+ it('does not re-render when only streamingText changes during token streaming', async () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [makeSession('session-1', 'Test Session', '/workspace/alpha', now)],
+ })
+ useChatStore.setState({
+ sessions: {
+ 'session-1': makeChatSessionState({ chatState: 'idle', streamingText: '' }),
+ },
+ })
+
+ const renders: string[] = []
+ const onRender: ProfilerOnRenderCallback = (_, phase) => { renders.push(phase) }
+
+ render(
+
+
+ ,
+ )
+
+ await act(async () => { await Promise.resolve() })
+ await act(async () => { await Promise.resolve() })
+ const initialCount = renders.length
+
+ // Simulate streaming token flush: only streamingText changes,
+ // creating a new top-level sessions reference (as updateSessionIn does)
+ act(() => {
+ const prev = useChatStore.getState().sessions
+ const session = prev['session-1']
+ useChatStore.setState({
+ sessions: { ...prev, 'session-1': { ...session, streamingText: 'Hello world' } },
+ })
+ })
+
+ expect(renders.length).toBe(initialCount)
+ })
+
+ it('does not re-render across multiple rapid streamingText updates', async () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [makeSession('session-1', 'Test Session', '/workspace/alpha', now)],
+ })
+ useChatStore.setState({
+ sessions: {
+ 'session-1': makeChatSessionState({ chatState: 'idle', streamingText: '' }),
+ },
+ })
+
+ const renders: string[] = []
+ const onRender: ProfilerOnRenderCallback = (_, phase) => { renders.push(phase) }
+
+ render(
+
+
+ ,
+ )
+
+ await act(async () => { await Promise.resolve() })
+ await act(async () => { await Promise.resolve() })
+ const initialCount = renders.length
+
+ // Simulate 5 rapid token flushes (as the 50ms interval would produce)
+ for (let i = 0; i < 5; i++) {
+ act(() => {
+ const prev = useChatStore.getState().sessions
+ const session = prev['session-1']
+ useChatStore.setState({
+ sessions: {
+ ...prev,
+ 'session-1': { ...session, streamingText: session.streamingText + ' token' + i },
+ },
+ })
+ })
+ }
+
+ expect(renders.length).toBe(initialCount)
+ })
+
+ it('does not re-render when only elapsedSeconds changes', async () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [makeSession('session-1', 'Test Session', '/workspace/alpha', now)],
+ })
+ useChatStore.setState({
+ sessions: {
+ 'session-1': makeChatSessionState({ chatState: 'idle', elapsedSeconds: 0 }),
+ },
+ })
+
+ const renders: string[] = []
+ const onRender: ProfilerOnRenderCallback = (_, phase) => { renders.push(phase) }
+
+ render(
+
+
+ ,
+ )
+
+ await act(async () => { await Promise.resolve() })
+ await act(async () => { await Promise.resolve() })
+ const initialCount = renders.length
+
+ act(() => {
+ const prev = useChatStore.getState().sessions
+ const session = prev['session-1']
+ useChatStore.setState({
+ sessions: { ...prev, 'session-1': { ...session, elapsedSeconds: 42 } },
+ })
+ })
+
+ expect(renders.length).toBe(initialCount)
+ })
+
+ it('re-renders and shows running indicator when chatState transitions to streaming', async () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [makeSession('session-1', 'Test Session', '/workspace/alpha', now)],
+ })
+ useChatStore.setState({
+ sessions: {
+ 'session-1': makeChatSessionState({ chatState: 'idle' }),
+ },
+ })
+
+ const renders: string[] = []
+ const onRender: ProfilerOnRenderCallback = (_, phase) => { renders.push(phase) }
+
+ render(
+
+
+ ,
+ )
+
+ await act(async () => { await Promise.resolve() })
+ await act(async () => { await Promise.resolve() })
+
+ expect(screen.queryByLabelText('Session running')).not.toBeInTheDocument()
+ const countBefore = renders.length
+
+ act(() => {
+ const prev = useChatStore.getState().sessions
+ const session = prev['session-1']
+ useChatStore.setState({
+ sessions: { ...prev, 'session-1': { ...session, chatState: 'streaming' } },
+ })
+ })
+
+ expect(renders.length).toBeGreaterThan(countBefore)
+ expect(screen.getByLabelText('Session running')).toBeInTheDocument()
+ })
+
+ it('re-renders and hides running indicator when chatState transitions back to idle', async () => {
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [makeSession('session-1', 'Test Session', '/workspace/alpha', now)],
+ })
+ useChatStore.setState({
+ sessions: {
+ 'session-1': makeChatSessionState({ chatState: 'streaming' }),
+ },
+ })
+
+ const renders: string[] = []
+ const onRender: ProfilerOnRenderCallback = (_, phase) => { renders.push(phase) }
+
+ render(
+
+
+ ,
+ )
+
+ await act(async () => { await Promise.resolve() })
+ await act(async () => { await Promise.resolve() })
+
+ expect(screen.getByLabelText('Session running')).toBeInTheDocument()
+ const countBefore = renders.length
+
+ act(() => {
+ const prev = useChatStore.getState().sessions
+ const session = prev['session-1']
+ useChatStore.setState({
+ sessions: { ...prev, 'session-1': { ...session, chatState: 'idle' } },
+ })
+ })
+
+ expect(renders.length).toBeGreaterThan(countBefore)
+ expect(screen.queryByLabelText('Session running')).not.toBeInTheDocument()
+ })
+ })
})