Merge sidebar workspaces across all connected hosts (#1538)

* Merge workspaces across all hosts in the sidebar

Replace per-host sidebar sections with a single merged project list.
Projects that exist on multiple hosts (matched by projectKey) are
collapsed into one row. Host identity is surfaced via:

- A status-only footer pill showing online/offline host counts
- Hover cards with a mandatory host row on every workspace entry
- Subtitles showing the host name when a project spans >1 host
- A host filter in the grouping dropdown (not a sidebar switcher)

Order and group-mode stores migrated from per-server to global keys
with zustand persist migrations. New-workspace screen now shows a
merged project list with a host selector combobox.

Key files:
- workspace-structure.ts: merge algorithm by projectKey
- sidebar-view-store.ts / sidebar-order-store.ts: global state
- sidebar-workspace-list.tsx: context-based subtitle injection
- new-workspace-screen.tsx: host selector in footer row

* Add host filtering to the merged sidebar

The sidebar now merges workspaces from every connected host, so a single
global "active host" no longer fits. Host selection becomes explicit per
action via a host chooser, and agent history pages across all hosts at
once. Removes the active-host / active-server-id model and the
all-agents-list hook that fed it.

* Fix rebase fallout: translate archive host error, update stale workspace-key test

archiveWorkspacesOptimistically used a raw "Host is not connected" string
that the i18n completeness test forbids; route it through i18n.t with the
existing hostDisconnected key. The selectors memoization test asserted
un-prefixed workspace keys, but merged-sidebar keys are serverId-prefixed.

* Fix sidebar review fallout

* Address sidebar review followups

* Fix sidebar workspace identity migration

* Address sidebar review followups

* Handle partial host history failures

* Fix merged sidebar project removal and E2E drift

* Restore history routes and project removal shape

* Remove sidebar host context plumbing

* Tighten sidebar status and host filter state

* Keep new workspace project selection host-compatible

* Migrate sidebar view storage

* Streamline host selection flows

* Update all-host history on archive

* Fix empty project sidebar action

Code regression: the main merge dropped the empty-project New workspace child row while keeping the empty project persistence contract. Restore the row using the project host target so the sidebar stays aligned with the no-host-selection model.

* Fix Japanese shortcut translation key

Stale-base integration: main added the Japanese locale after this branch added cycleAgentMode to the English shortcut help map. Merging main exposed the missing ja key in CI typecheck and resources.test.

* Fix Playwright speech teardown race
This commit is contained in:
Mohamed Boudra
2026-06-26 12:16:49 +08:00
committed by GitHub
parent 1109e453bc
commit a49c658d1f
103 changed files with 6856 additions and 2857 deletions
+165 -355
View File
@@ -1,18 +1,7 @@
import { router, usePathname } from "expo-router";
import { FolderPlus, History, Home, Plus, Search, Settings, X } from "lucide-react-native";
import { FolderPlus, History, Home, Plus, Search, Server, Settings, X } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import {
type Dispatch,
memo,
type ReactElement,
type RefObject,
type SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
import {
Pressable,
StyleSheet as RNStyleSheet,
@@ -20,8 +9,6 @@ import {
useWindowDimensions,
View,
type PressableStateCallbackType,
type StyleProp,
type ViewStyle,
} from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
@@ -34,40 +21,42 @@ import Animated, {
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { HostPicker } from "@/components/hosts/host-picker";
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
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 { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { useHostChooser } from "@/hosts/host-chooser";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
import {
type SidebarProjectEntry,
type SidebarStatusWorkspacePlacement,
useSidebarWorkspacesList,
} from "@/hooks/use-sidebar-workspaces-list";
import { useStatusModeWorkspacePlacements } from "@/hooks/use-status-mode-workspaces";
import { useSidebarViewStore, type SidebarGroupMode } from "@/stores/sidebar-view-store";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { useHostRuntimeSnapshot, useHosts } from "@/runtime/host-runtime";
import { useHosts } from "@/runtime/host-runtime";
import {
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
selectIsAgentListOpen,
usePanelStore,
} from "@/stores/panel-store";
import { resolveActiveHost } from "@/utils/active-host";
import { formatConnectionStatus } from "@/utils/daemons";
import { useWindowControlsPadding } from "@/utils/desktop-window";
import { canCloseLeftSidebarGesture } from "@/utils/sidebar-animation-state";
import {
buildHostOpenProjectRoute,
buildOpenProjectRoute,
buildHostNewWorkspaceRoute,
buildHostSessionsRoute,
buildSessionsRoute,
buildSettingsAddHostRoute,
buildSettingsHostSectionRoute,
buildSettingsRoute,
mapPathnameToServer,
} from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
@@ -85,14 +74,9 @@ interface LeftSidebarProps {
interface SidebarSharedProps {
theme: SidebarTheme;
activeServerId: string | null;
activeHostLabel: string;
activeHostStatusColor: string;
hostOptions: ComboboxOption[];
hostTriggerRef: RefObject<View | null>;
isHostPickerOpen: boolean;
setIsHostPickerOpen: Dispatch<SetStateAction<boolean>>;
statusWorkspacePlacements: SidebarStatusWorkspacePlacement[];
projects: SidebarProjectEntry[];
projectNamesByKey: Map<string, string>;
isInitialLoad: boolean;
isRevalidating: boolean;
isManualRefresh: boolean;
@@ -101,19 +85,14 @@ interface SidebarSharedProps {
shortcutIndexByWorkspaceKey: SidebarShortcutModel["shortcutIndexByWorkspaceKey"];
toggleProjectCollapsed: SidebarShortcutModel["toggleProjectCollapsed"];
handleRefresh: () => void;
handleHostSelect: (nextServerId: string) => void;
handleNewWorkspaceNavigate: () => void;
handleOpenProject: () => void;
handleHome: () => void;
handleSettings: () => void;
labels: SidebarLabels;
renderHostOption: (input: {
option: ComboboxOption;
selected: boolean;
active: boolean;
onPress: () => void;
}) => ReactElement;
newWorkspaceKeys: ShortcutKey[][] | null;
handleAddHost: () => void;
handleOpenHostSettings: (serverId: string) => void;
}
interface SidebarLabels {
@@ -154,70 +133,24 @@ export const LeftSidebar = memo(function LeftSidebar({
selectIsAgentListOpen(state, { isCompact: isCompactLayout }),
);
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const pathname = usePathname();
const daemons = useHosts();
const activeDaemon = useMemo(
() => resolveActiveHost({ hosts: daemons, pathname }),
[daemons, pathname],
);
const activeServerId = activeDaemon?.serverId ?? null;
const activeHostLabel = useMemo(() => {
if (!activeDaemon) return t("sidebar.host.noHost");
const trimmed = activeDaemon.label?.trim();
return trimmed && trimmed.length > 0 ? trimmed : activeDaemon.serverId;
}, [activeDaemon, t]);
const activeHostSnapshot = useHostRuntimeSnapshot(activeServerId ?? "");
const activeHostStatus = activeServerId
? (activeHostSnapshot?.connectionStatus ?? "connecting")
: "idle";
let activeHostStatusColor: string;
if (activeHostStatus === "online") activeHostStatusColor = theme.colors.palette.green[400];
else if (activeHostStatus === "connecting")
activeHostStatusColor = theme.colors.palette.amber[500];
else activeHostStatusColor = theme.colors.palette.red[500];
const hostOptions = useMemo(
() =>
daemons.map((daemon) => ({
id: daemon.serverId,
label: daemon.label?.trim() || daemon.serverId,
})),
[daemons],
);
const renderHostOption = useCallback(
({
option,
selected,
active,
onPress,
}: {
option: ComboboxOption;
selected: boolean;
active: boolean;
onPress: () => void;
}) => (
<HostSwitchOption
serverId={option.id}
label={option.label}
selected={selected}
active={active}
onPress={onPress}
/>
),
[],
);
const hostTriggerRef = useRef<View | null>(null);
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
const { projects, isInitialLoad, isRevalidating, refreshAll } = useSidebarWorkspacesList({
serverId: activeServerId,
const {
workspacePlacements,
projects,
projectNamesByKey,
isInitialLoad,
isRevalidating,
refreshAll,
} = useSidebarWorkspacesList({
enabled: isCompactLayout || isOpen,
});
const statusWorkspacePlacements = useStatusModeWorkspacePlacements({
placements: workspacePlacements,
});
const { collapsedProjectKeys, shortcutIndexByWorkspaceKey, toggleProjectCollapsed } =
useSidebarShortcutModel({ projects });
const groupMode = useSidebarViewStore((state) =>
activeServerId ? state.getGroupMode(activeServerId) : "project",
);
const groupMode = useSidebarViewStore((state) => state.groupMode);
const [isManualRefresh, setIsManualRefresh] = useState(false);
@@ -232,7 +165,8 @@ export const LeftSidebar = memo(function LeftSidebar({
}
}, [isRevalidating, isManualRefresh]);
const openProjectPicker = useOpenProjectPicker(activeServerId);
const openProjectPicker = useOpenProjectPicker();
const chooseHost = useHostChooser();
const handleOpenProjectMobile = useCallback(() => {
showMobileAgent();
@@ -244,9 +178,13 @@ export const LeftSidebar = memo(function LeftSidebar({
}, [openProjectPicker]);
const handleNewWorkspaceNavigate = useCallback(() => {
if (!activeServerId) return;
router.navigate(buildHostNewWorkspaceRoute(activeServerId));
}, [activeServerId]);
chooseHost({
title: "Choose host",
onChooseHost: (serverId) => {
router.push(buildHostNewWorkspaceRoute(serverId));
},
});
}, [chooseHost]);
const handleSettingsMobile = useCallback(() => {
showMobileAgent();
@@ -257,38 +195,41 @@ export const LeftSidebar = memo(function LeftSidebar({
router.push(buildSettingsRoute());
}, []);
const handleHomeMobile = useCallback(() => {
if (!activeServerId) return;
const handleAddHostMobile = useCallback(() => {
showMobileAgent();
router.push(buildHostOpenProjectRoute(activeServerId));
}, [activeServerId, showMobileAgent]);
router.push(buildSettingsAddHostRoute(Date.now()));
}, [showMobileAgent]);
const handleHomeDesktop = useCallback(() => {
if (!activeServerId) return;
router.push(buildHostOpenProjectRoute(activeServerId));
}, [activeServerId]);
const handleAddHostDesktop = useCallback(() => {
router.push(buildSettingsAddHostRoute(Date.now()));
}, []);
const handleViewMoreNavigate = useCallback(() => {
if (!activeServerId) {
return;
}
router.push(buildHostSessionsRoute(activeServerId));
}, [activeServerId]);
const handleHostSelect = useCallback(
(nextServerId: string) => {
if (!nextServerId) {
return;
}
const nextPath = mapPathnameToServer(pathname, nextServerId);
setIsHostPickerOpen(false);
router.push(nextPath);
const handleOpenHostSettingsMobile = useCallback(
(serverId: string) => {
showMobileAgent();
router.push(buildSettingsHostSectionRoute(serverId, "connections"));
},
[pathname],
[showMobileAgent],
);
const newWorkspaceKeys = useShortcutKeys("new-workspace");
const handleOpenHostSettingsDesktop = useCallback((serverId: string) => {
router.push(buildSettingsHostSectionRoute(serverId, "connections"));
}, []);
const handleHomeMobile = useCallback(() => {
showMobileAgent();
router.push(buildOpenProjectRoute());
}, [showMobileAgent]);
const handleHomeDesktop = useCallback(() => {
router.push(buildOpenProjectRoute());
}, []);
const handleViewMoreNavigate = useCallback(() => {
router.push(buildSessionsRoute());
}, []);
const newWorkspaceKeys = useShortcutKeys("new-workspace");
const labels = useMemo(
(): SidebarLabels => ({
addProject: t("sidebar.actions.addProject"),
@@ -305,14 +246,9 @@ export const LeftSidebar = memo(function LeftSidebar({
const sharedProps = {
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
statusWorkspacePlacements,
projects,
projectNamesByKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -321,8 +257,6 @@ export const LeftSidebar = memo(function LeftSidebar({
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
handleRefresh,
handleHostSelect,
renderHostOption,
labels,
newWorkspaceKeys,
};
@@ -339,6 +273,8 @@ export const LeftSidebar = memo(function LeftSidebar({
handleOpenProject={handleOpenProjectMobile}
handleHome={handleHomeMobile}
handleSettings={handleSettingsMobile}
handleAddHost={handleAddHostMobile}
handleOpenHostSettings={handleOpenHostSettingsMobile}
handleViewMoreNavigate={handleViewMoreNavigate}
/>
);
@@ -353,91 +289,41 @@ export const LeftSidebar = memo(function LeftSidebar({
handleOpenProject={handleOpenProjectDesktop}
handleHome={handleHomeDesktop}
handleSettings={handleSettingsDesktop}
handleAddHost={handleAddHostDesktop}
handleOpenHostSettings={handleOpenHostSettingsDesktop}
handleViewMore={handleViewMoreNavigate}
/>
);
});
interface HostPickerTriggerProps {
triggerRef: React.Ref<View>;
setIsHostPickerOpen: Dispatch<SetStateAction<boolean>>;
hostOptionsEmpty: boolean;
hostStatusDotStyle: StyleProp<ViewStyle>;
activeHostLabel: string;
function sidebarHostOptionTestID(serverId: string): string {
return `sidebar-host-row-${serverId}`;
}
function HostPickerTrigger({
triggerRef,
setIsHostPickerOpen,
hostOptionsEmpty,
hostStatusDotStyle,
activeHostLabel,
}: HostPickerTriggerProps) {
const pressableStyle = useCallback(
({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
styles.hostTrigger,
hovered && styles.hostTriggerHovered,
],
[],
);
const handlePress = useCallback(() => setIsHostPickerOpen(true), [setIsHostPickerOpen]);
return (
<Pressable
ref={triggerRef}
style={pressableStyle}
onPress={handlePress}
disabled={hostOptionsEmpty}
>
<View style={hostStatusDotStyle} />
<Text style={styles.hostTriggerText} numberOfLines={1}>
{activeHostLabel}
</Text>
</Pressable>
);
}
function HostSwitchOption({
serverId,
label,
selected,
active,
onPress,
}: {
serverId: string;
label: string;
selected: boolean;
active: boolean;
onPress: () => void;
}) {
const snapshot = useHostRuntimeSnapshot(serverId);
const connectionStatus = snapshot?.connectionStatus ?? "connecting";
return (
<ComboboxItem
label={label}
description={formatConnectionStatus(connectionStatus)}
selected={selected}
active={active}
onPress={onPress}
/>
);
function sidebarHostLocalMarkerTestID(serverId: string): string {
return `sidebar-host-local-marker-${serverId}`;
}
function FooterIconButton({
buttonRef,
onPress,
testID,
accessibilityLabel,
icon: Icon,
iconSize,
theme,
}: {
onPress: () => void;
testID: string;
accessibilityLabel: string;
icon: typeof FolderPlus;
iconSize?: number;
theme: SidebarTheme;
buttonRef?: RefObject<View | null>;
}) {
return (
<Pressable
ref={buttonRef}
style={styles.footerIconButton}
testID={testID}
nativeID={testID}
@@ -449,7 +335,7 @@ function FooterIconButton({
>
{({ hovered }) => (
<Icon
size={theme.iconSize.md}
size={iconSize ?? theme.iconSize.md}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
@@ -457,6 +343,59 @@ function FooterIconButton({
);
}
function SidebarHostPicker({
theme,
onAddHost,
onOpenHostSettings,
}: {
theme: SidebarTheme;
onAddHost: () => void;
onOpenHostSettings: (serverId: string) => void;
}) {
const hosts = useHosts();
const triggerRef = useRef<View | null>(null);
const [isOpen, setIsOpen] = useState(false);
const handleSelect = useCallback(
(id: string) => {
onOpenHostSettings(id);
},
[onOpenHostSettings],
);
const handleOpen = useCallback(() => setIsOpen(true), []);
return (
<HostPicker
hosts={hosts}
value=""
onSelect={handleSelect}
open={isOpen}
onOpenChange={setIsOpen}
anchorRef={triggerRef}
includeAddHost
onAddHost={onAddHost}
showLocalMarker
onOpenHostSettings={onOpenHostSettings}
searchable
desktopMinWidth={240}
addHostTestID="sidebar-host-add"
hostOptionTestID={sidebarHostOptionTestID}
hostLocalMarkerTestID={sidebarHostLocalMarkerTestID}
>
<FooterIconButton
buttonRef={triggerRef}
onPress={handleOpen}
testID="sidebar-hosts-trigger"
accessibilityLabel="Hosts"
icon={Server}
iconSize={theme.iconSize.sm}
theme={theme}
/>
</HostPicker>
);
}
function AddProjectTooltipContent({
newAgentKeys,
label,
@@ -489,30 +428,14 @@ function HeaderIconTooltipContent({
function SidebarFooter({
theme,
activeServerId,
activeHostLabel,
hostStatusDotStyle,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
handleHostSelect,
renderHostOption,
handleOpenProject,
handleHome,
handleSettings,
labels,
handleAddHost,
handleOpenHostSettings,
}: {
theme: SidebarTheme;
activeServerId: string | null;
activeHostLabel: string;
hostStatusDotStyle: StyleProp<ViewStyle>;
hostOptions: ComboboxOption[];
hostTriggerRef: RefObject<View | null>;
isHostPickerOpen: boolean;
setIsHostPickerOpen: Dispatch<SetStateAction<boolean>>;
handleHostSelect: (nextServerId: string) => void;
renderHostOption: SidebarSharedProps["renderHostOption"];
handleOpenProject: () => void;
handleHome: () => void;
handleSettings: () => void;
@@ -523,20 +446,19 @@ function SidebarFooter({
switchHost: string;
searchHosts: string;
};
handleAddHost: () => void;
handleOpenHostSettings: (serverId: string) => void;
}) {
const newAgentKeys = useShortcutKeys("new-agent");
return (
<View style={styles.sidebarFooter}>
<View style={styles.footerHostSlot}>
<HostPickerTrigger
triggerRef={hostTriggerRef}
setIsHostPickerOpen={setIsHostPickerOpen}
hostOptionsEmpty={hostOptions.length === 0}
hostStatusDotStyle={hostStatusDotStyle}
activeHostLabel={activeHostLabel}
/>
</View>
<View style={styles.footerIconRow}>
<SidebarHostPicker
theme={theme}
onAddHost={handleAddHost}
onOpenHostSettings={handleOpenHostSettings}
/>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<FooterIconButton
@@ -566,33 +488,15 @@ function SidebarFooter({
theme={theme}
/>
</View>
<Combobox
options={hostOptions}
value={activeServerId ?? ""}
onSelect={handleHostSelect}
renderOption={renderHostOption}
searchable={false}
title={labels.switchHost}
searchPlaceholder={labels.searchHosts}
desktopMinWidth={280}
open={isHostPickerOpen}
onOpenChange={setIsHostPickerOpen}
anchorRef={hostTriggerRef}
/>
</View>
);
}
function MobileSidebar({
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
statusWorkspacePlacements,
projects,
projectNamesByKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -601,14 +505,14 @@ function MobileSidebar({
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
handleRefresh,
handleHostSelect,
renderHostOption,
newWorkspaceKeys,
handleNewWorkspaceNavigate,
handleOpenProject,
handleHome,
handleSettings,
labels,
handleAddHost,
handleOpenHostSettings,
insetsTop,
insetsBottom,
isOpen,
@@ -638,21 +542,11 @@ function MobileSidebar({
}, [closeSidebar, gestureAnimatingRef]);
const handleViewMore = useCallback(() => {
if (!activeServerId) {
return;
}
translateX.value = -windowWidth;
backdropOpacity.value = 0;
closeSidebar();
handleViewMoreNavigate();
}, [
activeServerId,
backdropOpacity,
closeSidebar,
handleViewMoreNavigate,
translateX,
windowWidth,
]);
}, [backdropOpacity, closeSidebar, handleViewMoreNavigate, translateX, windowWidth]);
const handleWorkspacePress = useCallback(() => {
closeSidebar();
@@ -752,11 +646,6 @@ function MobileSidebar({
[windowWidth, insetsTop, insetsBottom],
);
const hostStatusDotStyle = useMemo(
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
[activeHostStatusColor],
);
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
@@ -826,7 +715,7 @@ function MobileSidebar({
variant="compact"
/>
</View>
<WorkspacesSectionHeader serverId={activeServerId} />
<WorkspacesSectionHeader />
<Pressable
style={styles.mobileCloseButton}
onPress={closeSidebar}
@@ -851,12 +740,13 @@ function MobileSidebar({
<SidebarAgentListSkeleton />
) : (
<SidebarWorkspaceList
serverId={activeServerId}
collapsedProjectKeys={collapsedProjectKeys}
onToggleProjectCollapsed={toggleProjectCollapsed}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
groupMode={groupMode}
statusWorkspacePlacements={statusWorkspacePlacements}
projects={projects}
projectNamesByKey={projectNamesByKey}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onWorkspacePress={handleWorkspacePress}
@@ -867,19 +757,12 @@ function MobileSidebar({
<SidebarFooter
theme={theme}
activeServerId={activeServerId}
activeHostLabel={activeHostLabel}
hostStatusDotStyle={hostStatusDotStyle}
hostOptions={hostOptions}
hostTriggerRef={hostTriggerRef}
isHostPickerOpen={isHostPickerOpen}
setIsHostPickerOpen={setIsHostPickerOpen}
handleHostSelect={handleHostSelect}
renderHostOption={renderHostOption}
handleOpenProject={handleOpenProject}
handleHome={handleHome}
handleSettings={handleSettings}
labels={labels}
handleAddHost={handleAddHost}
handleOpenHostSettings={handleOpenHostSettings}
/>
</View>
</Animated.View>
@@ -890,14 +773,9 @@ function MobileSidebar({
function DesktopSidebar({
theme,
activeServerId,
activeHostLabel,
activeHostStatusColor,
hostOptions,
hostTriggerRef,
isHostPickerOpen,
setIsHostPickerOpen,
statusWorkspacePlacements,
projects,
projectNamesByKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -906,14 +784,14 @@ function DesktopSidebar({
shortcutIndexByWorkspaceKey,
toggleProjectCollapsed,
handleRefresh,
handleHostSelect,
renderHostOption,
newWorkspaceKeys,
handleNewWorkspaceNavigate,
handleOpenProject,
handleHome,
handleSettings,
labels,
handleAddHost,
handleOpenHostSettings,
insetsTop,
isOpen,
handleViewMore,
@@ -924,10 +802,6 @@ function DesktopSidebar({
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth);
const { width: viewportWidth } = useWindowDimensions();
const hostStatusDotStyle = useMemo(
() => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
[activeHostStatusColor],
);
const startWidthRef = useRef(sidebarWidth);
const resizeWidth = useSharedValue(sidebarWidth);
@@ -1007,18 +881,19 @@ function DesktopSidebar({
/>
</View>
</View>
<WorkspacesSectionHeader serverId={activeServerId} />
<WorkspacesSectionHeader />
{isInitialLoad ? (
<SidebarAgentListSkeleton />
) : (
<SidebarWorkspaceList
serverId={activeServerId}
collapsedProjectKeys={collapsedProjectKeys}
onToggleProjectCollapsed={toggleProjectCollapsed}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
groupMode={groupMode}
statusWorkspacePlacements={statusWorkspacePlacements}
projects={projects}
projectNamesByKey={projectNamesByKey}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onAddProject={handleOpenProject}
@@ -1029,19 +904,12 @@ function DesktopSidebar({
<SidebarFooter
theme={theme}
activeServerId={activeServerId}
activeHostLabel={activeHostLabel}
hostStatusDotStyle={hostStatusDotStyle}
hostOptions={hostOptions}
hostTriggerRef={hostTriggerRef}
isHostPickerOpen={isHostPickerOpen}
setIsHostPickerOpen={setIsHostPickerOpen}
handleHostSelect={handleHostSelect}
renderHostOption={renderHostOption}
handleOpenProject={handleOpenProject}
handleHome={handleHome}
handleSettings={handleSettings}
labels={labels}
handleAddHost={handleAddHost}
handleOpenHostSettings={handleOpenHostSettings}
/>
{/* Resize handle - absolutely positioned over right border */}
@@ -1053,7 +921,7 @@ function DesktopSidebar({
);
}
function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
function WorkspacesSectionHeader() {
const { theme } = useUnistyles();
const setCommandCenterOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen);
const commandCenterKeys = useShortcutKeys("toggle-command-center");
@@ -1096,7 +964,7 @@ function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<View>
<SidebarDisplayPreferencesMenu serverId={serverId} />
<SidebarDisplayPreferencesMenu />
</View>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
@@ -1208,45 +1076,15 @@ const styles = StyleSheet.create((theme) => ({
sidebarDragArea: {
position: "relative",
},
hostTrigger: {
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
gap: theme.spacing[2],
minWidth: 0,
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
hostTriggerHovered: {
backgroundColor: theme.colors.surfaceSidebarHover,
},
hostStatusDot: {
width: 8,
height: 8,
borderRadius: theme.borderRadius.full,
},
hostTriggerText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
flexShrink: 1,
minWidth: 0,
},
sidebarFooter: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
justifyContent: "flex-start",
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
footerHostSlot: {
flexGrow: 0,
flexShrink: 1,
minWidth: 0,
marginRight: theme.spacing[2],
},
footerIconRow: {
flexDirection: "row",
alignItems: "center",
@@ -1261,34 +1099,6 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[1],
},
hostPickerList: {
gap: theme.spacing[2],
},
hostPickerOption: {
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface2,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
hostPickerOptionText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
hostPickerCancel: {
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface0,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
alignItems: "center",
},
hostPickerCancelText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
tooltipRow: {
flexDirection: "row",
alignItems: "center",