Isolate branch switcher query from WorkspaceScreenContent to prevent 20ms re-renders

Move useBranchSwitcher into BranchSwitcher component so query state
changes only re-render the small header widget instead of the entire
workspace screen. Also memo-wrap AgentStatusBar to skip re-renders
on keystroke.
This commit is contained in:
Mohamed Boudra
2026-04-12 12:38:46 +07:00
parent cde10485d4
commit acb294b9eb
4 changed files with 43 additions and 37 deletions
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from "react"; import { memo, useCallback, useMemo, useRef, useState } from "react";
import { View, Text, Platform, Pressable, Keyboard } from "react-native"; import { View, Text, Platform, Pressable, Keyboard } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow"; import { useShallow } from "zustand/shallow";
@@ -848,7 +848,11 @@ function ControlledStatusBar({
const EMPTY_MODES: AgentMode[] = []; const EMPTY_MODES: AgentMode[] = [];
export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStatusBarProps) { export const AgentStatusBar = memo(function AgentStatusBar({
agentId,
serverId,
onDropdownClose,
}: AgentStatusBarProps) {
const { preferences, updatePreferences } = useFormPreferences(); const { preferences, updatePreferences } = useFormPreferences();
const agent = useSessionStore( const agent = useSessionStore(
useShallow((state) => { useShallow((state) => {
@@ -1048,7 +1052,7 @@ export function AgentStatusBar({ agentId, serverId, onDropdownClose }: AgentStat
disabled={!client} disabled={!client}
/> />
); );
} });
export function DraftAgentStatusBar({ export function DraftAgentStatusBar({
providerDefinitions, providerDefinitions,
+28 -11
View File
@@ -1,28 +1,45 @@
import { useRef } from "react"; import { useRef } from "react";
import { Pressable, Text, View } from "react-native"; import { Pressable, Text, View } from "react-native";
import { useQueryClient } from "@tanstack/react-query";
import { ChevronDown, GitBranch } from "lucide-react-native"; import { ChevronDown, GitBranch } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useToast } from "@/contexts/toast-context";
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
interface BranchSwitcherProps { interface BranchSwitcherProps {
currentBranchName: string | null; currentBranchName: string | null;
title: string; title: string;
branchOptions: ComboboxOption[]; serverId: string;
isOpen: boolean; workspaceId: string;
onOpenChange: (open: boolean) => void; isGitCheckout: boolean;
onBranchSelect: (branchId: string) => void;
} }
export function BranchSwitcher({ export function BranchSwitcher({
currentBranchName, currentBranchName,
title, title,
branchOptions, serverId,
isOpen, workspaceId,
onOpenChange, isGitCheckout,
onBranchSelect,
}: BranchSwitcherProps) { }: BranchSwitcherProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const anchorRef = useRef<View>(null); const anchorRef = useRef<View>(null);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const toast = useToast();
const queryClient = useQueryClient();
const { branchOptions, isOpen, setIsOpen, handleBranchSelect } = useBranchSwitcher({
client,
normalizedServerId: serverId,
normalizedWorkspaceId: workspaceId,
currentBranchName,
isGitCheckout,
isConnected,
toast,
queryClient,
});
if (!currentBranchName) { if (!currentBranchName) {
return ( return (
@@ -36,7 +53,7 @@ export function BranchSwitcher({
<View ref={anchorRef} collapsable={false}> <View ref={anchorRef} collapsable={false}>
<Pressable <Pressable
testID="workspace-header-branch-switcher" testID="workspace-header-branch-switcher"
onPress={() => onOpenChange(true)} onPress={() => setIsOpen(true)}
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.branchSwitcherTrigger, styles.branchSwitcherTrigger,
(hovered || pressed) && styles.branchSwitcherTriggerHovered, (hovered || pressed) && styles.branchSwitcherTriggerHovered,
@@ -53,14 +70,14 @@ export function BranchSwitcher({
<Combobox <Combobox
options={branchOptions} options={branchOptions}
value={currentBranchName} value={currentBranchName}
onSelect={onBranchSelect} onSelect={handleBranchSelect}
searchable searchable
placeholder="Switch branch..." placeholder="Switch branch..."
searchPlaceholder="Filter branches..." searchPlaceholder="Filter branches..."
emptyText="No branches found." emptyText="No branches found."
title="Switch branch" title="Switch branch"
open={isOpen} open={isOpen}
onOpenChange={onOpenChange} onOpenChange={setIsOpen}
anchorRef={anchorRef} anchorRef={anchorRef}
desktopPlacement="bottom-start" desktopPlacement="bottom-start"
desktopPreventInitialFlash desktopPreventInitialFlash
@@ -81,7 +81,6 @@ import type { ListTerminalsResponse } from "@server/shared/messages";
import { upsertTerminalListEntry } from "@/utils/terminal-list"; import { upsertTerminalListEntry } from "@/utils/terminal-list";
import { confirmDialog } from "@/utils/confirm-dialog"; import { confirmDialog } from "@/utils/confirm-dialog";
import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
import { useStableEvent } from "@/hooks/use-stable-event"; import { useStableEvent } from "@/hooks/use-stable-event";
import { buildProviderCommand } from "@/utils/provider-command-templates"; import { buildProviderCommand } from "@/utils/provider-command-templates";
import { generateDraftId } from "@/stores/draft-keys"; import { generateDraftId } from "@/stores/draft-keys";
@@ -762,23 +761,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
? trimNonEmpty(checkoutQuery.data.currentBranch) ? trimNonEmpty(checkoutQuery.data.currentBranch)
: null; : null;
const {
branchOptions,
isOpen: isBranchSwitcherOpen,
setIsOpen: setIsBranchSwitcherOpen,
handleBranchSelect,
invalidateStashAndCheckout,
} = useBranchSwitcher({
client,
normalizedServerId,
normalizedWorkspaceId,
currentBranchName,
isGitCheckout,
isConnected,
toast,
queryClient,
});
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
@@ -1982,10 +1964,9 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
<BranchSwitcher <BranchSwitcher
currentBranchName={currentBranchName} currentBranchName={currentBranchName}
title={workspaceHeader.title} title={workspaceHeader.title}
branchOptions={branchOptions} serverId={normalizedServerId}
isOpen={isBranchSwitcherOpen} workspaceId={normalizedWorkspaceId}
onOpenChange={setIsBranchSwitcherOpen} isGitCheckout={isGitCheckout}
onBranchSelect={handleBranchSelect}
/> />
<Text <Text
testID="workspace-header-subtitle" testID="workspace-header-subtitle"
+5 -1
View File
@@ -5876,7 +5876,11 @@ export class Session {
}; };
const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId); const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId);
if (lastEmitted && lastEmitted.kind === "upsert" && equal(lastEmitted.workspace, nextWorkspace)) { if (
lastEmitted &&
lastEmitted.kind === "upsert" &&
equal(lastEmitted.workspace, nextWorkspace)
) {
continue; continue;
} }