Reduce workspace render churn

This commit is contained in:
Mohamed Boudra
2026-06-12 21:53:22 +07:00
parent 889d058658
commit c4d544baf6
11 changed files with 252 additions and 99 deletions
+53 -26
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,