Reduce workspace render churn

This commit is contained in:
Mohamed Boudra
2026-06-12 21:21:32 +07:00
parent 889d058658
commit c4d544baf6
11 changed files with 252 additions and 99 deletions

View File

@@ -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<StreamItem[], Map<string, StreamLayoutItem[]>>();
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,

View File

@@ -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<AgentStreamViewHandle, AgentStreamVi
handleInlinePathPress({ raw: filePath, path: filePath }, "main");
});
// Freeze stream data while this tab slot is hidden to prevent offscreen FlatList
// cell-window renders on every 48ms flush from background agents.
// When isActive flips back to true, the context change triggers a re-render and
// the component reads the current (fresh) streamItems/streamHead from props.
const isActive = useContext(MountedTabActiveContext);
const frozenStreamItemsRef = useRef(streamItems);
const frozenStreamHeadRef = useRef(streamHead);
if (isActive) {
frozenStreamItemsRef.current = streamItems;
frozenStreamHeadRef.current = streamHead;
}
const effectiveStreamItems = isActive ? streamItems : frozenStreamItemsRef.current;
const effectiveStreamHead = isActive ? streamHead : frozenStreamHeadRef.current;
const baseRenderModel = useMemo(() => {
return buildAgentStreamRenderModel({
agentStatus: agent.status,
tail: streamItems,
head: streamHead ?? EMPTY_STREAM_HEAD,
tail: effectiveStreamItems,
head: effectiveStreamHead ?? EMPTY_STREAM_HEAD,
platform: isWeb ? "web" : "native",
isMobileBreakpoint: isMobile,
});
}, [agent.status, isMobile, streamHead, streamItems]);
}, [agent.status, isMobile, effectiveStreamHead, effectiveStreamItems]);
const streamLayout = useMemo(
() =>
layoutStream({
@@ -684,14 +700,17 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
(item) => renderHistoryRow(item),
[renderHistoryRow],
);
const renderLiveHeadRow = useCallback<StreamSegmentRenderers["renderLiveHeadRow"]>(
(item) =>
// useStableEvent keeps the function reference stable across flushes.
// layoutLiveHeadItemById and renderStreamItem are read from the ref at call time,
// so the live-head render always uses the latest layout without causing renderers
// to be a new object on every text-chunk flush.
const renderLiveHeadRow: StreamSegmentRenderers["renderLiveHeadRow"] = useStableEvent(
(item: StreamItem) =>
renderLiveHeadStreamItem({
item,
layoutItemById: layoutLiveHeadItemById,
renderStreamItem,
}),
[layoutLiveHeadItemById, renderStreamItem],
);
const renderLiveAuxiliary = useCallback<StreamSegmentRenderers["renderLiveAuxiliary"]>(() => {
return renderLiveAuxiliaryNode({

View File

@@ -41,7 +41,10 @@ import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isWeb } from "@/constants/platform";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
import {
useSidebarAnimation,
useSidebarSettledGeneration,
} from "@/contexts/sidebar-animation-context";
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
@@ -615,12 +618,12 @@ function MobileSidebar({
windowWidth,
animateToOpen,
animateToClose,
settledGeneration,
isGesturing,
mobilePanelState,
gestureAnimatingRef,
closeGestureRef,
} = useSidebarAnimation();
const settledGeneration = useSidebarSettledGeneration();
const closeTouchStartX = useSharedValue(0);
const closeTouchStartY = useSharedValue(0);

View File

@@ -1,5 +1,6 @@
import {
Fragment,
createContext,
memo,
useCallback,
useEffect,
@@ -73,6 +74,10 @@ import { RenderProfile } from "@/utils/render-profiler";
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
import { isNative } from "@/constants/platform";
// true = this tab slot is the active (visible) tab; false = mounted but hidden.
// Defaults to true so consumers outside a slot (e.g. web preview) are unaffected.
export const MountedTabActiveContext = createContext<boolean>(true);
interface SplitContainerProps {
layout: WorkspaceLayout;
workspaceKey: string;
@@ -219,14 +224,16 @@ const MountedTabSlot = memo(function MountedTabSlot({
return (
<RenderProfile id={`DesktopMountedTabSlot:${tabDescriptor.kind}:${tabDescriptor.tabId}`}>
<View style={wrapperStyle}>
<WorkspacePaneContent
content={content}
isWorkspaceFocused={isWorkspaceFocused}
isPaneFocused={isPaneFocused}
onFocusPane={handleFocusPane}
/>
</View>
<MountedTabActiveContext value={isVisible}>
<View style={wrapperStyle}>
<WorkspacePaneContent
content={content}
isWorkspaceFocused={isWorkspaceFocused}
isPaneFocused={isPaneFocused}
onFocusPane={handleFocusPane}
/>
</View>
</MountedTabActiveContext>
</RenderProfile>
);
});

View File

@@ -20,14 +20,20 @@ export function WorkspaceShortcutTargetsSubscriber({
serverId: string | null;
}) {
const { projects } = useSidebarWorkspacesList({ serverId, enabled });
const statusWorkspaces = useStatusModeWorkspaceEntries({
serverId: enabled ? serverId : null,
projects,
});
const projectNamesByKey = useProjectNamesMap(enabled ? serverId : null);
// groupMode must be resolved before gating the status-mode subscriptions below.
const groupMode = useSidebarViewStore((state) =>
enabled && serverId ? state.getGroupMode(serverId) : "project",
);
// Only subscribe to agents/workspaces when the status-group view is actually active.
// In project mode (the default), these subscriptions would fire on every agent update
// (agents Map identity is replaced on every status transition) with no effect on
// the shortcut targets, causing ~15-46 wasted re-renders per agent switch.
const isStatusMode = enabled && groupMode === "status";
const statusWorkspaces = useStatusModeWorkspaceEntries({
serverId: isStatusMode ? serverId : null,
projects,
});
const projectNamesByKey = useProjectNamesMap(isStatusMode ? serverId : null);
const collapsedProjectKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedProjectKeys,
);

View File

@@ -50,7 +50,6 @@ interface SidebarAnimationContextValue {
animateToClose: () => void;
startMobilePanelTransition: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
settleMobilePanel: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
settledGeneration: number;
isGesturing: SharedValue<boolean>;
mobileVisualPanel: SharedValue<number>;
mobilePanelState: SharedValue<number>;
@@ -61,6 +60,10 @@ interface SidebarAnimationContextValue {
const SidebarAnimationContext = createContext<SidebarAnimationContextValue | null>(null);
// Separate context so that settle-driven re-renders only reach MobileSidebar,
// not every other useSidebarAnimation() consumer.
const SidebarSettledGenerationContext = createContext<number>(0);
function getMobileVisualPanel(mobileView: "agent" | "agent-list" | "file-explorer"): number {
if (mobileView === "agent-list") {
return MOBILE_VISUAL_PANEL_AGENT_LIST;
@@ -357,7 +360,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
animateToClose,
startMobilePanelTransition,
settleMobilePanel,
settledGeneration,
isGesturing,
mobileVisualPanel,
mobilePanelState,
@@ -373,7 +375,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
animateToClose,
startMobilePanelTransition,
settleMobilePanel,
settledGeneration,
isGesturing,
mobileVisualPanel,
mobilePanelState,
@@ -384,7 +385,11 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
);
return (
<SidebarAnimationContext.Provider value={value}>{children}</SidebarAnimationContext.Provider>
<SidebarAnimationContext.Provider value={value}>
<SidebarSettledGenerationContext.Provider value={settledGeneration}>
{children}
</SidebarSettledGenerationContext.Provider>
</SidebarAnimationContext.Provider>
);
}
@@ -395,3 +400,7 @@ export function useSidebarAnimation() {
}
return context;
}
export function useSidebarSettledGeneration() {
return useContext(SidebarSettledGenerationContext);
}

View File

@@ -2,6 +2,7 @@ import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useSyncExternalStore,
type ReactNode,
@@ -57,22 +58,27 @@ export function useVoice() {
export function useVoiceOptional(): VoiceContextValue | null {
const runtime = useContext(VoiceRuntimeContext);
const snapshot = useSyncExternalStore(
runtime ? runtime.subscribe.bind(runtime) : noopSubscribe,
runtime ? runtime.getSnapshot.bind(runtime) : getEmptySnapshot,
runtime ? runtime.getSnapshot.bind(runtime) : getEmptySnapshot,
runtime ? runtime.subscribe : noopSubscribe,
runtime ? runtime.getSnapshot : getEmptySnapshot,
runtime ? runtime.getSnapshot : getEmptySnapshot,
);
if (!runtime) {
return null;
}
return {
...snapshot,
startVoice: runtime.startVoice.bind(runtime),
stopVoice: runtime.stopVoice.bind(runtime),
isVoiceModeForAgent: runtime.isVoiceModeForAgent.bind(runtime),
toggleMute: runtime.toggleMute.bind(runtime),
};
// Methods on the runtime object literal close over factory-local state; they
// don't use `this`, so no binding is needed. Memoising on [snapshot, runtime]
// keeps the returned object reference stable across re-renders that don't
// change either, preventing downstream memo/useMemo misses.
return useMemo(() => {
if (!runtime) {
return null;
}
return {
...snapshot,
startVoice: runtime.startVoice,
stopVoice: runtime.stopVoice,
isVoiceModeForAgent: runtime.isVoiceModeForAgent,
toggleMute: runtime.toggleMute,
};
}, [snapshot, runtime]);
}
export function useVoiceTelemetry() {

View File

@@ -1,4 +1,5 @@
import { useMemo, useCallback, useSyncExternalStore } from "react";
import { useMemo, useCallback, useRef, useSyncExternalStore } from "react";
import equal from "fast-deep-equal";
import { useShallow } from "zustand/shallow";
import { useSessionStore } from "@/stores/session-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory";
@@ -44,6 +45,13 @@ export function useAggregatedAgents(options?: {
runtime.refreshAllAgentDirectories();
}, [runtime]);
// Keyed by "serverId:agentId" — reuse the previous AggregatedAgent object when
// none of its fields changed, so downstream memo/shallow comparisons can bail early.
const prevAgentsRef = useRef<Map<string, AggregatedAgent>>(new Map());
// Preserved sorted array — returned as-is when every element kept its identity
// and order, so callers using reference equality skip re-renders entirely.
const prevSortedRef = useRef<AggregatedAgent[]>([]);
const result = useMemo(() => {
// runtimeVersion is referenced so the memo recomputes when runtime state changes.
void runtimeVersion;
@@ -79,7 +87,11 @@ export function useAggregatedAgents(options?: {
createdAt: agent.createdAt,
labels: agent.labels,
};
allAgents.push(nextAgent);
const cacheKey = `${serverId}:${agent.id}`;
const prev = prevAgentsRef.current.get(cacheKey);
// Preserve object identity when fields are unchanged so callers can use
// reference equality (useShallow, memo) to skip re-renders.
allAgents.push(prev !== undefined && equal(prev, nextAgent) ? prev : nextAgent);
}
}
@@ -98,8 +110,25 @@ export function useAggregatedAgents(options?: {
return rightTime - leftTime;
});
// Update the identity cache for the next render pass.
const nextCache = new Map<string, AggregatedAgent>();
for (const agent of allAgents) {
nextCache.set(`${agent.serverId}:${agent.id}`, agent);
}
prevAgentsRef.current = nextCache;
// If every element kept its reference identity and the order is the same,
// return the previous array so downstream reference comparisons can bail.
const prevSorted = prevSortedRef.current;
const stableAgents =
allAgents.length === prevSorted.length &&
allAgents.every((agent, i) => agent === prevSorted[i])
? prevSorted
: allAgents;
prevSortedRef.current = stableAgents;
// Check if we have any cached data
const hasAnyData = allAgents.length > 0;
const hasAnyData = stableAgents.length > 0;
// Align list loading with the runtime directory-sync machine.
const isLoading = daemons.some((daemon) => {
@@ -111,7 +140,7 @@ export function useAggregatedAgents(options?: {
const isRevalidating = isLoading && hasAnyData;
return {
agents: allAgents,
agents: stableAgents,
isLoading,
isInitialLoad,
isRevalidating,

View File

@@ -1,7 +1,9 @@
import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
import equal from "fast-deep-equal";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useCreateFlowStore, type PendingCreateAttempt } from "@/stores/create-flow-store";
import { useSessionStore, type Agent, type WorkspaceDescriptor } from "@/stores/session-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { selectWorkspace, workspaceEqualityFns } from "@/stores/session-store-hooks/selectors";
import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state";
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
import { selectPrHintFromStatus } from "@/git/use-pr-status-query";
@@ -147,22 +149,31 @@ export function useSidebarWorkspaceEntry(
serverId: string | null,
workspaceId: string | null,
): SidebarWorkspaceEntry | null {
const pendingCreateAttempts = useCreateFlowStore((state) => state.pendingByDraftId);
const agents = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.agents : undefined,
// Deep-compare so that adding/removing unrelated pending creates doesn't re-render this row.
const pendingCreateAttempts = useStoreWithEqualityFn(
useCreateFlowStore,
(state) => state.pendingByDraftId,
workspaceEqualityFns.deep,
);
const projectWorkspaceEntry = useCallback(
(workspace: WorkspaceDescriptor): SidebarWorkspaceEntry =>
createSidebarWorkspaceEntry({
// Single subscription: reads workspace + agents together, computes the full entry, and
// deep-compares the output. Agents-Map identity churn (setAgents replaces the Map on every
// status transition) never causes a React re-render unless the derived entry actually changes.
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
const workspace = selectWorkspace(state, serverId, workspaceId);
if (!workspace) return null;
const agents = serverId ? state.sessions[serverId]?.agents : undefined;
return createSidebarWorkspaceEntry({
serverId: serverId ?? "",
workspace,
pendingCreateAttempts,
agents,
}),
[agents, pendingCreateAttempts, serverId],
});
},
equal,
);
return useWorkspaceFields(serverId, workspaceId, projectWorkspaceEntry);
}
const EMPTY_ORDER: string[] = [];

View File

@@ -22,7 +22,12 @@ import type { UserComposerAttachment } from "@/attachments/types";
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
import type { ImageAttachment } from "@/composer/types";
import { getProviderIcon } from "@/components/provider-icons";
import { ToastViewport, useToastHost } from "@/components/toast-host";
import {
ToastViewport,
useToastHost,
type ToastApi,
type ToastState,
} from "@/components/toast-host";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import {
useWorkspaceAttachments,
@@ -682,7 +687,7 @@ function ChatAgentContent({
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
}) {
const { t } = useTranslation();
const panelToast = useToastHost();
const { api: toastApi, toast: toastState, dismiss: dismissToast } = useToastHost();
const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef<AgentStreamViewHandle>(null);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
@@ -737,10 +742,10 @@ function ChatAgentContent({
addFilesRef.current?.(uploaded);
} catch (error) {
console.error("[AgentPanel] Failed to upload dropped files:", error);
panelToast.api.error(error instanceof Error ? error.message : "Failed to upload file");
toastApi.error(error instanceof Error ? error.message : "Failed to upload file");
}
},
[client, isConnected, panelToast.api],
[client, isConnected, toastApi],
);
const agentState = useSessionStore(
@@ -841,7 +846,7 @@ function ChatAgentContent({
if (connectionStatus === "online") {
if (reconnectToastArmedRef.current) {
reconnectToastArmedRef.current = false;
panelToast.dismiss();
dismissToast();
}
return;
}
@@ -850,12 +855,12 @@ function ChatAgentContent({
}
if (!reconnectToastArmedRef.current) {
reconnectToastArmedRef.current = true;
panelToast.api.show(t("agentPanel.states.reconnecting"), {
toastApi.show(t("agentPanel.states.reconnecting"), {
durationMs: null,
testID: "agent-reconnecting-toast",
});
}
}, [connectionStatus, panelToast, t]);
}, [connectionStatus, dismissToast, toastApi, t]);
useEffect(() => {
if (!isPaneFocused || !agentId || !isConnected || !hasSession) {
@@ -1089,7 +1094,9 @@ function ChatAgentContent({
effectiveAgent={effectiveAgent}
routeBottomAnchorRequest={routeBottomAnchorRequest}
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
panelToast={panelToast}
toastApi={toastApi}
toast={toastState}
dismiss={dismissToast}
streamViewRef={streamViewRef}
animatedContentStyle={animatedContentStyle}
handleFilesDropped={handleFilesDropped}
@@ -1100,7 +1107,8 @@ function ChatAgentContent({
handleMessageSent={handleMessageSent}
showHistorySyncOverlay={showHistorySyncOverlay}
cwd={agentCwd}
attentionController={attentionController}
onAttentionInputFocus={attentionController.clearOnInputFocus}
onAttentionPromptSend={attentionController.clearOnPromptSend}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
);
@@ -1124,7 +1132,8 @@ function isImagePath(path: string): boolean {
]);
return imageExts.has(ext);
}
function ChatAgentReadyContent({
const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
serverId,
agentId,
isPaneFocused,
@@ -1133,7 +1142,9 @@ function ChatAgentReadyContent({
effectiveAgent,
routeBottomAnchorRequest,
hasAppliedAuthoritativeHistory,
panelToast,
toastApi,
toast,
dismiss,
streamViewRef,
animatedContentStyle,
handleFilesDropped,
@@ -1144,7 +1155,8 @@ function ChatAgentReadyContent({
handleMessageSent,
showHistorySyncOverlay,
cwd,
attentionController,
onAttentionInputFocus,
onAttentionPromptSend,
onOpenWorkspaceFile,
}: {
serverId: string;
@@ -1155,7 +1167,9 @@ function ChatAgentReadyContent({
effectiveAgent: AgentScreenAgent;
routeBottomAnchorRequest: RouteBottomAnchorRequest;
hasAppliedAuthoritativeHistory: boolean;
panelToast: ReturnType<typeof useToastHost>;
toastApi: ToastApi;
toast: ToastState | null;
dismiss: () => void;
streamViewRef: React.RefObject<AgentStreamViewHandle | null>;
animatedContentStyle: object[];
handleFilesDropped: (files: ImageAttachment[]) => void;
@@ -1166,16 +1180,33 @@ function ChatAgentReadyContent({
handleMessageSent: () => void;
showHistorySyncOverlay: boolean;
cwd: string;
attentionController: ReturnType<typeof useAgentAttentionClear>;
onAttentionInputFocus: () => void;
onAttentionPromptSend: () => void;
onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void;
}) {
const { t } = useTranslation();
const agentInputDraft = useAgentInputDraft({
const rawAgentInputDraft = useAgentInputDraft({
draftKey: buildDraftStoreKey({
serverId,
agentId,
}),
});
// Stabilize the agentInputDraft object identity so that memo(AgentComposerSection) can bail out
// when only toast state changes (which does not affect any draft field).
const { text, setText, attachments, setAttachments, clear, isHydrated, composerState } =
rawAgentInputDraft;
const agentInputDraft = useMemo(
(): AgentInputDraft => ({
text,
setText,
attachments,
setAttachments,
clear,
isHydrated,
composerState,
}),
[text, setText, attachments, setAttachments, clear, isHydrated, composerState],
);
const streamSection = (
<RenderProfile id={`AgentStreamSection:${agentId}`}>
<AgentStreamSection
@@ -1185,7 +1216,7 @@ function ChatAgentReadyContent({
agent={effectiveAgent}
routeBottomAnchorRequest={routeBottomAnchorRequest}
hasAppliedAuthoritativeHistory={hasAppliedAuthoritativeHistory}
toast={panelToast.api}
toast={toastApi}
onOpenWorkspaceFile={onOpenWorkspaceFile}
/>
</RenderProfile>
@@ -1201,8 +1232,8 @@ function ChatAgentReadyContent({
cwd={cwd}
isSubmitLoading={false}
agentInputDraft={agentInputDraft}
onAttentionInputFocus={attentionController.clearOnInputFocus}
onAttentionPromptSend={attentionController.clearOnPromptSend}
onAttentionInputFocus={onAttentionInputFocus}
onAttentionPromptSend={onAttentionPromptSend}
onAddImages={handleAddImagesCallback}
onAddFiles={handleAddFilesCallback}
onComposerHeightChange={handleComposerHeightChange}
@@ -1234,11 +1265,7 @@ function ChatAgentReadyContent({
</View>
) : null}
<ToastViewport
toast={panelToast.toast}
onDismiss={panelToast.dismiss}
placement="panel"
/>
<ToastViewport toast={toast} onDismiss={dismiss} placement="panel" />
</View>
</FileDropZone>
@@ -1252,7 +1279,7 @@ function ChatAgentReadyContent({
</View>
</RewindComposerRestoreProvider>
);
}
});
const AgentStreamSection = memo(function AgentStreamSection({
streamViewRef,
@@ -1320,7 +1347,7 @@ const AgentStreamSection = memo(function AgentStreamSection({
);
});
function AgentComposerSection({
const AgentComposerSection = memo(function AgentComposerSection({
agentId,
serverId,
isPaneFocused,
@@ -1377,7 +1404,7 @@ function AgentComposerSection({
onMessageSent={onMessageSent}
/>
);
}
});
function ActiveAgentComposer({
agentId,

View File

@@ -60,7 +60,7 @@ import {
FloatingPanelPortalHostNameProvider,
} from "@/components/ui/floating-panel-portal";
import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { SplitContainer } from "@/components/split-container";
import { MountedTabActiveContext, SplitContainer } from "@/components/split-container";
import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon";
import { WorkspaceGitActions } from "@/git/workspace-actions";
import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open-in-editor-button";
@@ -855,13 +855,15 @@ const MobileMountedTabSlot = memo(function MobileMountedTabSlot({
return (
<RenderProfile id={`MobileMountedTabSlot:${tabDescriptor.kind}:${tabDescriptor.tabId}`}>
<View style={slotStyle} pointerEvents={isVisible ? "auto" : "none"}>
<WorkspacePaneContent
content={content}
isWorkspaceFocused={isWorkspaceFocused}
isPaneFocused={isPaneFocused}
/>
</View>
<MountedTabActiveContext value={isVisible}>
<View style={slotStyle} pointerEvents={isVisible ? "auto" : "none"}>
<WorkspacePaneContent
content={content}
isWorkspaceFocused={isWorkspaceFocused}
isPaneFocused={isPaneFocused}
/>
</View>
</MountedTabActiveContext>
</RenderProfile>
);
});