Fix workspace tab presentation hook ordering

This commit is contained in:
Mohamed Boudra
2026-03-19 15:45:29 +07:00
parent 7609ccee4c
commit 20fa1a3a3b
4 changed files with 193 additions and 141 deletions
+33 -27
View File
@@ -41,7 +41,7 @@ import {
type WorkspaceDesktopTabRowItem, type WorkspaceDesktopTabRowItem,
} from "@/screens/workspace/workspace-desktop-tabs-row"; } from "@/screens/workspace/workspace-desktop-tabs-row";
import { import {
useWorkspaceTabPresentation, WorkspaceTabPresentationResolver,
WorkspaceTabIcon, WorkspaceTabIcon,
} from "@/screens/workspace/workspace-tab-presentation"; } from "@/screens/workspace/workspace-tab-presentation";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
@@ -489,35 +489,41 @@ function DragOverlayTabChipInner({
normalizedWorkspaceId: string; normalizedWorkspaceId: string;
}) { }) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const presentation = useWorkspaceTabPresentation({
tab,
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
});
const label =
presentation.titleState === "loading" ? "Loading..." : presentation.label;
return ( return (
<View <WorkspaceTabPresentationResolver
style={[ tab={tab}
styles.dragOverlayChip, serverId={normalizedServerId}
{ workspaceId={normalizedWorkspaceId}
backgroundColor: theme.colors.surface1,
borderColor: theme.colors.borderAccent,
},
]}
> >
<WorkspaceTabIcon presentation={presentation} active size={14} /> {(presentation) => {
<Text const label =
numberOfLines={1} presentation.titleState === "loading" ? "Loading..." : presentation.label;
style={[
styles.dragOverlayLabel, return (
{ color: theme.colors.foreground }, <View
]} style={[
> styles.dragOverlayChip,
{label} {
</Text> backgroundColor: theme.colors.surface1,
</View> borderColor: theme.colors.borderAccent,
},
]}
>
<WorkspaceTabIcon presentation={presentation} active size={14} />
<Text
numberOfLines={1}
style={[
styles.dragOverlayLabel,
{ color: theme.colors.foreground },
]}
>
{label}
</Text>
</View>
);
}}
</WorkspaceTabPresentationResolver>
); );
} }
@@ -15,7 +15,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout"; import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout";
import { import {
useWorkspaceTabPresentation, WorkspaceTabPresentationResolver,
WorkspaceTabIcon, WorkspaceTabIcon,
type WorkspaceTabPresentation, type WorkspaceTabPresentation,
} from "@/screens/workspace/workspace-tab-presentation"; } from "@/screens/workspace/workspace-tab-presentation";
@@ -546,13 +546,6 @@ function ResolvedDesktopTabChip({
showDropIndicatorBefore: boolean; showDropIndicatorBefore: boolean;
showDropIndicatorAfter: boolean; showDropIndicatorAfter: boolean;
}) { }) {
const presentation = useWorkspaceTabPresentation({
tab: item.tab,
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
});
const tooltipLabel =
presentation.titleState === "loading" ? "Loading agent title" : presentation.label;
const resolvedTab = useMemo( const resolvedTab = useMemo(
() => () =>
buildWorkspaceDesktopTabActions({ buildWorkspaceDesktopTabActions({
@@ -580,33 +573,46 @@ function ResolvedDesktopTabChip({
); );
return ( return (
<View style={styles.tabSlot}> <WorkspaceTabPresentationResolver
{showDropIndicatorBefore ? ( tab={item.tab}
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} /> serverId={normalizedServerId}
) : null} workspaceId={normalizedWorkspaceId}
<TabChip >
tab={item.tab} {(presentation) => {
isActive={item.isActive} const tooltipLabel =
isDragging={isDragging} presentation.titleState === "loading" ? "Loading agent title" : presentation.label;
isFocused={isFocused}
resolvedTabWidth={resolvedTabWidth} return (
showLabel={showLabel} <View style={styles.tabSlot}>
showCloseButton={showCloseButton} {showDropIndicatorBefore ? (
isCloseHovered={item.isCloseHovered} <View style={[styles.tabDropIndicator, styles.tabDropIndicatorBefore]} />
isClosingTab={item.isClosingTab} ) : null}
presentation={presentation} <TabChip
tooltipLabel={tooltipLabel} tab={item.tab}
resolvedTab={resolvedTab} isActive={item.isActive}
setHoveredTabKey={setHoveredTabKey} isDragging={isDragging}
setHoveredCloseTabKey={setHoveredCloseTabKey} isFocused={isFocused}
onNavigateTab={onNavigateTab} resolvedTabWidth={resolvedTabWidth}
onCloseTab={onCloseTab} showLabel={showLabel}
dragHandleProps={dragHandleProps} showCloseButton={showCloseButton}
/> isCloseHovered={item.isCloseHovered}
{showDropIndicatorAfter ? ( isClosingTab={item.isClosingTab}
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} /> presentation={presentation}
) : null} tooltipLabel={tooltipLabel}
</View> resolvedTab={resolvedTab}
setHoveredTabKey={setHoveredTabKey}
setHoveredCloseTabKey={setHoveredCloseTabKey}
onNavigateTab={onNavigateTab}
onCloseTab={onCloseTab}
dragHandleProps={dragHandleProps}
/>
{showDropIndicatorAfter ? (
<View style={[styles.tabDropIndicator, styles.tabDropIndicatorAfter]} />
) : null}
</View>
);
}}
</WorkspaceTabPresentationResolver>
); );
} }
@@ -83,7 +83,7 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
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";
import { import {
useWorkspaceTabPresentation, WorkspaceTabPresentationResolver,
WorkspaceTabIcon, WorkspaceTabIcon,
WorkspaceTabOptionRow, WorkspaceTabOptionRow,
} from "@/screens/workspace/workspace-tab-presentation"; } from "@/screens/workspace/workspace-tab-presentation";
@@ -226,22 +226,24 @@ function ResolvedMobileActiveTabTrigger({
normalizedServerId: string; normalizedServerId: string;
normalizedWorkspaceId: string; normalizedWorkspaceId: string;
}) { }) {
const presentation = useWorkspaceTabPresentation({
tab: activeTab,
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
});
return ( return (
<> <WorkspaceTabPresentationResolver
<View style={styles.switcherTriggerIcon} testID="workspace-active-tab-icon"> tab={activeTab}
<WorkspaceTabIcon presentation={presentation} active /> serverId={normalizedServerId}
</View> workspaceId={normalizedWorkspaceId}
>
{(presentation) => (
<>
<View style={styles.switcherTriggerIcon} testID="workspace-active-tab-icon">
<WorkspaceTabIcon presentation={presentation} active />
</View>
<Text style={styles.switcherTriggerText} numberOfLines={1}> <Text style={styles.switcherTriggerText} numberOfLines={1}>
{presentation.titleState === "loading" ? "Loading..." : presentation.label} {presentation.titleState === "loading" ? "Loading..." : presentation.label}
</Text> </Text>
</> </>
)}
</WorkspaceTabPresentationResolver>
); );
} }
@@ -277,11 +279,6 @@ function MobileWorkspaceTabOption({
onCloseOtherTabs: (tabId: string) => Promise<void> | void; onCloseOtherTabs: (tabId: string) => Promise<void> | void;
}) { }) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const presentation = useWorkspaceTabPresentation({
tab,
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
});
const menuTestIDBase = `workspace-tab-menu-${tab.key}`; const menuTestIDBase = `workspace-tab-menu-${tab.key}`;
const menuEntries = buildWorkspaceTabMenuEntries({ const menuEntries = buildWorkspaceTabMenuEntries({
surface: "mobile", surface: "mobile",
@@ -298,53 +295,61 @@ function MobileWorkspaceTabOption({
}); });
return ( return (
<WorkspaceTabOptionRow <WorkspaceTabPresentationResolver
presentation={presentation} tab={tab}
selected={selected} serverId={normalizedServerId}
active={active} workspaceId={normalizedWorkspaceId}
onPress={onPress} >
trailingAccessory={ {(presentation) => (
<DropdownMenu> <WorkspaceTabOptionRow
<DropdownMenuTrigger presentation={presentation}
testID={`${menuTestIDBase}-trigger`} selected={selected}
accessibilityRole="button" active={active}
accessibilityLabel={`Open menu for ${presentation.label}`} onPress={onPress}
hitSlop={8} trailingAccessory={
style={({ open, pressed }) => [ <DropdownMenu>
styles.mobileTabMenuTrigger, <DropdownMenuTrigger
(open || pressed) && styles.mobileTabMenuTriggerActive, testID={`${menuTestIDBase}-trigger`}
]} accessibilityRole="button"
> accessibilityLabel={`Open menu for ${presentation.label}`}
<Ellipsis hitSlop={8}
size={theme.iconSize.sm} style={({ open, pressed }) => [
color={theme.colors.foregroundMuted} styles.mobileTabMenuTrigger,
/> (open || pressed) && styles.mobileTabMenuTriggerActive,
</DropdownMenuTrigger> ]}
<DropdownMenuContent >
side="bottom" <Ellipsis
align="end" size={theme.iconSize.sm}
width={220} color={theme.colors.foregroundMuted}
testID={menuTestIDBase} />
> </DropdownMenuTrigger>
{menuEntries.map((entry) => <DropdownMenuContent
entry.kind === "separator" ? ( side="bottom"
<DropdownMenuSeparator key={entry.key} /> align="end"
) : ( width={220}
<DropdownMenuItem testID={menuTestIDBase}
key={entry.key} >
testID={entry.testID} {menuEntries.map((entry) =>
disabled={entry.disabled} entry.kind === "separator" ? (
destructive={entry.destructive} <DropdownMenuSeparator key={entry.key} />
onSelect={entry.onSelect} ) : (
> <DropdownMenuItem
{entry.label} key={entry.key}
</DropdownMenuItem> testID={entry.testID}
) disabled={entry.disabled}
)} destructive={entry.destructive}
</DropdownMenuContent> onSelect={entry.onSelect}
</DropdownMenu> >
} {entry.label}
/> </DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
}
/>
)}
</WorkspaceTabPresentationResolver>
); );
} }
@@ -21,23 +21,56 @@ export interface WorkspaceTabPresentation {
statusBucket: SidebarStateBucket | null; statusBucket: SidebarStateBucket | null;
} }
export function useWorkspaceTabPresentation(input: { type WorkspaceTabPresentationResolverProps = {
tab: WorkspaceTabDescriptor; tab: WorkspaceTabDescriptor;
serverId: string; serverId: string;
workspaceId: string; workspaceId: string;
}): WorkspaceTabPresentation { children: (presentation: WorkspaceTabPresentation) => ReactNode;
};
type WorkspaceTabPresentationResolverInnerProps = WorkspaceTabPresentationResolverProps & {
registration: NonNullable<ReturnType<typeof getPanelRegistration>>;
};
export function WorkspaceTabPresentationResolver({
tab,
serverId,
workspaceId,
children,
}: WorkspaceTabPresentationResolverProps): ReactElement {
ensurePanelsRegistered(); ensurePanelsRegistered();
const registration = getPanelRegistration(input.tab.kind); const registration = getPanelRegistration(tab.kind);
invariant(registration, `No panel registration for kind: ${input.tab.kind}`); invariant(registration, `No panel registration for kind: ${tab.kind}`);
const descriptor = registration.useDescriptor(input.tab.target, {
serverId: input.serverId, return (
workspaceId: input.workspaceId, <WorkspaceTabPresentationResolverInner
key={tab.kind}
registration={registration}
tab={tab}
serverId={serverId}
workspaceId={workspaceId}
>
{children}
</WorkspaceTabPresentationResolverInner>
);
}
function WorkspaceTabPresentationResolverInner({
registration,
tab,
serverId,
workspaceId,
children,
}: WorkspaceTabPresentationResolverInnerProps): ReactElement {
const descriptor = registration.useDescriptor(tab.target as never, {
serverId,
workspaceId,
}); });
return useMemo( const presentation = useMemo(
() => ({ () => ({
key: input.tab.key, key: tab.key,
kind: input.tab.kind, kind: tab.kind,
label: descriptor.label, label: descriptor.label,
subtitle: descriptor.subtitle, subtitle: descriptor.subtitle,
titleState: descriptor.titleState, titleState: descriptor.titleState,
@@ -50,10 +83,12 @@ export function useWorkspaceTabPresentation(input: {
descriptor.statusBucket, descriptor.statusBucket,
descriptor.subtitle, descriptor.subtitle,
descriptor.titleState, descriptor.titleState,
input.tab.key, tab.key,
input.tab.kind, tab.kind,
] ]
); );
return <>{children(presentation)}</>;
} }
type WorkspaceTabIconProps = { type WorkspaceTabIconProps = {