Keep workspace focus mode scoped and easy to exit (#2151)

* fix(workspace): keep focus mode scoped and easy to exit

Route focus mode through the active workspace so persisted state cannot hide chrome on settings or other screens. Add a visible exit control and keep desktop window chrome aligned and visually quiet.

* fix(app): clarify muted chrome semantics
This commit is contained in:
Mohamed Boudra
2026-07-16 23:46:24 +02:00
committed by GitHub
parent d2308f4835
commit a622860a3e
25 changed files with 261 additions and 30 deletions
+6 -1
View File
@@ -1081,7 +1081,12 @@ function SettingsSidebar({
);
return (
<View style={outerContainerStyle} testID="settings-sidebar">
<View
accessibilityLabel={t("settings.title")}
role="navigation"
style={outerContainerStyle}
testID="settings-sidebar"
>
{isDesktop ? (
<View style={innerContainerStyle}>
<View style={sidebarStyles.sidebarDragArea}>
@@ -438,6 +438,8 @@ interface WorkspaceDesktopTabsRowProps {
activeDragTabId?: string | null;
tabDropPreviewIndex?: number | null;
showPaneSplitActions?: boolean;
focusModeEnabled: boolean;
onExitFocusMode: () => void;
}
function getFallbackTabLabel(
@@ -758,15 +760,19 @@ export function WorkspaceDesktopTabsRow({
activeDragTabId = null,
tabDropPreviewIndex = null,
showPaneSplitActions = true,
focusModeEnabled,
onExitFocusMode,
}: WorkspaceDesktopTabsRowProps) {
const { t } = useTranslation();
const router = useRouter();
const newTabKeys = useShortcutKeys("workspace-tab-new");
const focusModeKeys = useShortcutKeys("toggle-focus");
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
const [exitFocusModeWidth, setExitFocusModeWidth] = useState<number>(0);
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
updateMeasuredWidth(setTabsContainerWidth, event);
@@ -780,12 +786,18 @@ export function WorkspaceDesktopTabsRow({
updateMeasuredWidth(setInlineAddButtonWidth, event);
}, []);
const handleExitFocusModeLayout = useCallback((event: LayoutChangeEvent) => {
updateMeasuredWidth(setExitFocusModeWidth, event);
}, []);
const layoutMetrics = useMemo(
() => ({
rowHorizontalInset: 0,
actionsReservedWidth: Math.max(
0,
tabsActionsWidth + (inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH),
tabsActionsWidth +
(inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH) +
(focusModeEnabled ? exitFocusModeWidth : 0),
),
rowPaddingHorizontal: 0,
tabGap: 0,
@@ -795,7 +807,7 @@ export function WorkspaceDesktopTabsRow({
estimatedCharWidth: 7,
closeButtonWidth: 22,
}),
[inlineAddButtonWidth, tabsActionsWidth],
[exitFocusModeWidth, focusModeEnabled, inlineAddButtonWidth, tabsActionsWidth],
);
const fallbackTabLabels = useMemo(
@@ -968,6 +980,31 @@ export function WorkspaceDesktopTabsRow({
testID="workspace-tabs-row"
onLayout={handleTabsContainerLayout}
>
{focusModeEnabled ? (
<View style={styles.exitFocusModeSlot} onLayout={handleExitFocusModeLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-exit-focus-mode"
onPress={onExitFocusMode}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.exitFocusMode")}
style={inlineAddActionButtonStyle}
>
<ThemedX size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>
{t("workspace.tabs.actions.exitFocusMode")}
</Text>
{focusModeKeys ? (
<Shortcut chord={focusModeKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
</View>
) : null}
<ScrollView
horizontal
scrollEnabled={layout.requiresHorizontalScrollFallback}
@@ -1184,6 +1221,14 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
paddingHorizontal: theme.spacing[2],
},
exitFocusModeSlot: {
alignSelf: "stretch",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: theme.spacing[1],
borderRightWidth: 1,
borderRightColor: theme.colors.border,
},
inlineAddButton: {
flexDirection: "row",
alignItems: "center",
@@ -266,6 +266,9 @@ const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
const extraMutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundExtraMuted,
});
const sourceControlPanelStrokeWidth15 = { strokeWidth: 1.5 };
@@ -1712,6 +1715,7 @@ function WorkspaceScreenContent({
const toast = useToast();
const isMobile = useIsCompactFormFactor();
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
const normalizedServerId = useMemo(() => trimNonEmpty(decodeSegment(serverId)) ?? "", [serverId]);
@@ -2966,6 +2970,11 @@ function WorkspaceScreenContent({
const handleWorkspacePaneAction = useCallback(
(action: KeyboardActionDefinition): boolean => {
if (action.id === "workspace.focus.toggle") {
toggleFocusMode();
return true;
}
if (!persistenceKey || !workspaceLayout) {
return true;
}
@@ -3039,6 +3048,7 @@ function WorkspaceScreenContent({
persistenceKey,
focusedPaneTabState.activeTabId,
focusedPaneTabState.pane,
toggleFocusMode,
workspaceLayout,
],
);
@@ -3072,6 +3082,7 @@ function WorkspaceScreenContent({
"workspace.pane.move-tab.up",
"workspace.pane.move-tab.down",
"workspace.pane.close",
"workspace.focus.toggle",
] as const,
enabled: Boolean(isRouteFocused && normalizedServerId && normalizedWorkspaceId),
priority: 100,
@@ -3366,8 +3377,8 @@ function WorkspaceScreenContent({
style={explorerToggleStyle}
>
{({ hovered, pressed }) => {
const active = isExplorerOpen || hovered || pressed;
const colorMapping = active ? foregroundColorMapping : mutedColorMapping;
const active = hovered || pressed;
const colorMapping = active ? foregroundColorMapping : extraMutedColorMapping;
return (
<>
<ThemedSourceControlPanelIcon size={16} uniProps={colorMapping} />
@@ -3412,9 +3423,9 @@ function WorkspaceScreenContent({
accessibilityLabel={explorerToggleLabel}
accessibilityState={explorerToggleAccessibilityState}
>
{({ hovered }) => {
{({ hovered, pressed }) => {
const colorMapping =
isExplorerOpen || hovered ? foregroundColorMapping : mutedColorMapping;
hovered || pressed ? foregroundColorMapping : extraMutedColorMapping;
return <ThemedPanelRight size={16} uniProps={colorMapping} />;
}}
</HeaderToggleButton>
@@ -3502,6 +3513,7 @@ function WorkspaceScreenContent({
<SplitContainer
layout={workspaceLayout}
focusModeEnabled={desktopFocusModeEnabled}
onExitFocusMode={toggleFocusMode}
workspaceKey={persistenceKey}
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
@@ -3539,6 +3551,7 @@ function WorkspaceScreenContent({
workspaceLayout,
persistenceKey,
desktopFocusModeEnabled,
toggleFocusMode,
normalizedServerId,
normalizedWorkspaceId,
isRouteFocused,
@@ -3668,6 +3681,8 @@ function WorkspaceScreenContent({
onSplitRight={noop}
onSplitDown={noop}
showPaneSplitActions={false}
focusModeEnabled={desktopFocusModeEnabled}
onExitFocusMode={toggleFocusMode}
/>
) : null}