From 6194d3fad3b53e6c2b80c422fe98043145494655 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 26 Jun 2026 02:24:31 +0800 Subject: [PATCH] perf(web): reuse sessions reference to stop sidebar re-render on streaming deltas (#1113) --- .changeset/web-sidebar-sessions-ref.md | 8 ++++ apps/kimi-web/src/api/daemon/eventReducer.ts | 8 +++- apps/kimi-web/test/event-reducer.test.ts | 41 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/web-sidebar-sessions-ref.md diff --git a/.changeset/web-sidebar-sessions-ref.md b/.changeset/web-sidebar-sessions-ref.md new file mode 100644 index 000000000..79f912498 --- /dev/null +++ b/.changeset/web-sidebar-sessions-ref.md @@ -0,0 +1,8 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Keep the web session sidebar from re-rendering on every streaming token. The +event reducer now reuses the `sessions` array reference for events that do not +change sessions, so the sidebar computeds (`sessionsForView` / `workspaceGroups` +/ `mergedWorkspaces`) are no longer dirtied by unrelated high-frequency events. diff --git a/apps/kimi-web/src/api/daemon/eventReducer.ts b/apps/kimi-web/src/api/daemon/eventReducer.ts index 23a94e002..47d9c4b47 100644 --- a/apps/kimi-web/src/api/daemon/eventReducer.ts +++ b/apps/kimi-web/src/api/daemon/eventReducer.ts @@ -84,7 +84,13 @@ export function createInitialState(): KimiClientState { function cloneState(s: KimiClientState): KimiClientState { return { ...s, - sessions: [...s.sessions], + // Reuse the `sessions` array reference when an event does not touch it. + // Every session-mutating case below already builds its own array via + // `[...]` / `.map` / `.filter`, so sharing the reference is safe — and it + // keeps `rawState.sessions` stable for events that don't change sessions, + // so the sidebar computeds (sessionsForView / workspaceGroups / + // mergedWorkspaces) are not dirtied by unrelated events. + sessions: s.sessions, messagesBySession: { ...s.messagesBySession }, approvalsBySession: { ...s.approvalsBySession }, planReviewByToolCallId: { ...s.planReviewByToolCallId }, diff --git a/apps/kimi-web/test/event-reducer.test.ts b/apps/kimi-web/test/event-reducer.test.ts index 4df40777e..068c7ea8c 100644 --- a/apps/kimi-web/test/event-reducer.test.ts +++ b/apps/kimi-web/test/event-reducer.test.ts @@ -149,3 +149,44 @@ describe('reduceAppEvent taskProgress', () => { expect(lines?.at(-1)).toBe('line 59'); }); }); + +describe('reduceAppEvent sessions reference stability', () => { + // The sidebar computeds (sessionsForView / workspaceGroups / mergedWorkspaces) + // depend on `rawState.sessions`. Events that do not change sessions must keep + // the SAME array reference so those computeds are not dirtied; events that do + // change sessions must produce a NEW array. + + it('reuses the sessions reference for an event that does not touch sessions', () => { + const state = { + ...createInitialState(), + sessions: [makeSession('s1', '2026-01-01T00:00:00.000Z')], + messagesBySession: { s1: [makeMessage('s1', '2026-01-01T00:00:00.000Z')] }, + }; + const next = reduceAppEvent( + state, + { + type: 'messageUpdated', + sessionId: 's1', + messageId: 'msg_2026-01-01T00:00:00.000Z', + content: [{ type: 'text', text: 'updated' }], + status: 'completed', + }, + { sessionId: 's1', seq: 2 }, + ); + expect(next.sessions).toBe(state.sessions); + }); + + it('produces a new sessions array for an event that changes sessions', () => { + const state = { + ...createInitialState(), + sessions: [makeSession('s1', '2026-01-01T00:00:00.000Z')], + }; + const next = reduceAppEvent( + state, + { type: 'sessionCreated', session: makeSession('s2', '2026-02-01T00:00:00.000Z') }, + { sessionId: 's2', seq: 3 }, + ); + expect(next.sessions).not.toBe(state.sessions); + expect(next.sessions.map((s) => s.id)).toEqual(['s2', 's1']); + }); +});