From c4d544baf63c433e113cbfd8a023cd8a051cf9af Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 12 Jun 2026 21:21:32 +0700 Subject: [PATCH] Reduce workspace render churn --- packages/app/src/agent-stream/layout.ts | 56 ++++++++++--- packages/app/src/agent-stream/view.tsx | 31 ++++++-- packages/app/src/components/left-sidebar.tsx | 7 +- .../app/src/components/split-container.tsx | 23 ++++-- .../workspace-shortcut-targets-subscriber.tsx | 16 ++-- .../contexts/sidebar-animation-context.tsx | 17 +++- packages/app/src/contexts/voice-context.tsx | 34 ++++---- .../app/src/hooks/use-aggregated-agents.ts | 37 ++++++++- .../src/hooks/use-sidebar-workspaces-list.ts | 33 +++++--- packages/app/src/panels/agent-panel.tsx | 79 +++++++++++++------ .../screens/workspace/workspace-screen.tsx | 18 +++-- 11 files changed, 252 insertions(+), 99 deletions(-) diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts index 1c6e83882..e70021342 100644 --- a/packages/app/src/agent-stream/layout.ts +++ b/packages/app/src/agent-stream/layout.ts @@ -206,6 +206,11 @@ function layoutSegment(input: LayoutSegmentInput): StreamLayoutItem[] { }); } +// Keyed by history array identity; inner key encodes the inputs that affect history layout. +// History layout is stable across text-chunk flushes because the liveHead boundary item's +// kind and id don't change when only its text grows. +const historyLayoutCache = new WeakMap>(); + export function layoutStream(input: StreamLayoutInput): StreamLayout { const auxiliaryTurnFooter = resolveAuxiliaryTurnFooter(input); const historyBoundaryIndex = input.strategy.getHistoryLiveBoundaryIndex(input.history); @@ -215,17 +220,46 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout { const liveHeadBoundaryItem = liveHeadBoundaryIndex === null ? null : (input.liveHead[liveHeadBoundaryIndex] ?? null); const frameOrder = input.strategy.getFrameChildOrder(); - const history = layoutSegment({ - strategy: input.strategy, - agentStatus: input.agentStatus, - items: input.history, - timingByAssistantId: input.timingByAssistantId, - auxiliaryTurnFooter, - frameOrder, - boundaryIndex: historyBoundaryIndex, - boundaryAboveItem: null, - boundaryBelowItem: liveHeadBoundaryItem, - }); + + let history: StreamLayoutItem[]; + if (input.history.length > 0) { + // The cache key encodes every input that can change history layout. liveHeadBoundaryItem.id + // and .kind are stable across text-only flushes (text growth doesn't change what kind of + // item borders history), so cached layout stays valid between flushes. + const historyCacheKey = [ + input.agentStatus, + frameOrder, + historyBoundaryIndex ?? "null", + liveHeadBoundaryItem?.id ?? "null", + liveHeadBoundaryItem?.kind ?? "null", + auxiliaryTurnFooter?.itemId ?? "null", + ].join(":"); + let byKey = historyLayoutCache.get(input.history); + if (!byKey) { + byKey = new Map(); + historyLayoutCache.set(input.history, byKey); + } + const cached = byKey.get(historyCacheKey); + if (cached) { + history = cached; + } else { + history = layoutSegment({ + strategy: input.strategy, + agentStatus: input.agentStatus, + items: input.history, + timingByAssistantId: input.timingByAssistantId, + auxiliaryTurnFooter, + frameOrder, + boundaryIndex: historyBoundaryIndex, + boundaryAboveItem: null, + boundaryBelowItem: liveHeadBoundaryItem, + }); + byKey.set(historyCacheKey, history); + } + } else { + history = []; + } + const liveHead = layoutSegment({ strategy: input.strategy, agentStatus: input.agentStatus, diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index e05f50f2d..7461b81c1 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, memo, useCallback, + useContext, useEffect, useImperativeHandle, useMemo, @@ -80,6 +81,7 @@ import { useStableEvent } from "@/hooks/use-stable-event"; import { isWeb } from "@/constants/platform"; import type { Theme } from "@/styles/theme"; import { recordRenderProfileReasons } from "@/utils/render-profiler"; +import { MountedTabActiveContext } from "@/components/split-container"; function renderLiveAuxiliaryNode(input: { pendingPermissions: ReactNode; @@ -364,15 +366,29 @@ const AgentStreamViewComponent = forwardRef