Track active workspace from navigation state

This commit is contained in:
Mohamed Boudra
2026-03-18 21:50:24 +07:00
parent 9d370a18b8
commit d15a1451b7
3 changed files with 133 additions and 44 deletions
+29 -1
View File
@@ -1,6 +1,12 @@
import "@/styles/unistyles";
import { polyfillCrypto } from "@/polyfills/crypto";
import { Stack, useGlobalSearchParams, usePathname, useRouter } from "expo-router";
import {
Stack,
useGlobalSearchParams,
useNavigationContainerRef,
usePathname,
useRouter,
} from "expo-router";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
@@ -68,6 +74,7 @@ import {
} from "@/utils/host-routes";
import { getTauri } from "@/utils/tauri";
import { attachConsole } from "@/utils/tauri-attach-console";
import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store";
polyfillCrypto();
attachConsole();
@@ -501,6 +508,26 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
);
}
function NavigationActiveWorkspaceObserver() {
const navigationRef = useNavigationContainerRef();
useEffect(() => {
syncNavigationActiveWorkspace(navigationRef);
const unsubscribeState = navigationRef.addListener("state", () => {
syncNavigationActiveWorkspace(navigationRef);
});
const unsubscribeReady = navigationRef.addListener("ready" as never, () => {
syncNavigationActiveWorkspace(navigationRef);
});
return () => {
unsubscribeState();
unsubscribeReady();
};
}, [navigationRef]);
return null;
}
function LoadingView({ message }: { message?: string } = {}) {
return (
<View
@@ -557,6 +584,7 @@ export default function RootLayout() {
<GestureHandlerRootView
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
>
<NavigationActiveWorkspaceObserver />
<PortalProvider>
<SafeAreaProvider>
<KeyboardProvider>
@@ -20,7 +20,7 @@ import {
type ReactElement,
type MutableRefObject,
} from 'react'
import { router, useGlobalSearchParams, usePathname } from 'expo-router'
import { router, usePathname } from 'expo-router'
import { navigateToWorkspace } from '@/hooks/use-workspace-navigation'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { type GestureType } from 'react-native-gesture-handler'
@@ -35,10 +35,8 @@ import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
import {
buildHostNewAgentRoute,
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
} from '@/utils/host-routes'
import { normalizeWorkspaceIdentity } from '@/utils/workspace-identity'
import {
type SidebarProjectEntry,
type SidebarWorkspaceEntry,
@@ -73,6 +71,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { buildSidebarProjectRowModel } from '@/utils/sidebar-project-row-model'
import { useNavigationActiveWorkspaceSelection } from '@/stores/navigation-active-workspace-store'
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
if (!icon) {
@@ -1212,10 +1211,7 @@ export function SidebarWorkspaceList({
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
const isNative = Platform.OS !== 'web'
const pathname = usePathname()
const routeParams = useGlobalSearchParams<{
serverId?: string | string[]
workspaceId?: string | string[]
}>()
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection()
const isTauri = getIsTauri()
const altDown = useKeyboardShortcutsStore((state) => state.altDown)
const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown)
@@ -1226,40 +1222,8 @@ export function SidebarWorkspaceList({
const getWorkspaceOrder = useSidebarOrderStore((state) => state.getWorkspaceOrder)
const setWorkspaceOrder = useSidebarOrderStore((state) => state.setWorkspaceOrder)
const activeWorkspaceSelection = useMemo(() => {
if (!pathname) {
return null
}
if (!parseHostWorkspaceRouteFromPathname(pathname)) {
return null
}
const serverValue = Array.isArray(routeParams.serverId)
? routeParams.serverId[0]
: routeParams.serverId
const workspaceValue = Array.isArray(routeParams.workspaceId)
? routeParams.workspaceId[0]
: routeParams.workspaceId
const serverId = serverValue?.trim() ?? ''
const workspaceId = normalizeWorkspaceIdentity(
workspaceValue ? decodeWorkspaceIdFromPathSegment(workspaceValue) : null
)
if (!serverId || !workspaceId) {
const parsed = parseHostWorkspaceRouteFromPathname(pathname)
if (!parsed) {
return null
}
return {
serverId: parsed.serverId,
workspaceId: parsed.workspaceId,
}
}
return {
serverId,
workspaceId,
}
}, [pathname, routeParams.serverId, routeParams.workspaceId])
const isWorkspaceRoute = useMemo(() => Boolean(pathname && parseHostWorkspaceRouteFromPathname(pathname)), [pathname])
const effectiveActiveWorkspaceSelection = isWorkspaceRoute ? activeWorkspaceSelection : null
const projectIconRequests = useMemo(() => {
if (!serverId) {
@@ -1409,7 +1373,7 @@ export function SidebarWorkspaceList({
displayName={item.projectName}
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
serverId={serverId}
activeWorkspaceSelection={activeWorkspaceSelection}
activeWorkspaceSelection={effectiveActiveWorkspaceSelection}
showShortcutBadges={showShortcutBadges}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
parentGestureRef={parentGestureRef}
@@ -1425,8 +1389,8 @@ export function SidebarWorkspaceList({
)
},
[
activeWorkspaceSelection,
collapsedProjectKeys,
effectiveActiveWorkspaceSelection,
handleCreateWorktree,
handleWorkspaceReorder,
onWorkspacePress,
@@ -0,0 +1,97 @@
import { useSyncExternalStore } from "react";
import {
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
} from "@/utils/host-routes";
interface ActiveWorkspaceSelection {
serverId: string;
workspaceId: string;
}
type NavigationRouteLike = {
name?: unknown;
params?: unknown;
path?: unknown;
};
interface NavigationObserverRef {
current: {
getCurrentRoute(): unknown;
} | null;
}
let snapshot: ActiveWorkspaceSelection | null = null;
const listeners = new Set<() => void>();
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function getSnapshot(): ActiveWorkspaceSelection | null {
return snapshot;
}
function emitIfChanged(next: ActiveWorkspaceSelection | null) {
if (
snapshot?.serverId === next?.serverId &&
snapshot?.workspaceId === next?.workspaceId
) {
return;
}
snapshot = next;
for (const listener of listeners) {
listener();
}
}
function extractActiveWorkspaceFromRoute(route: NavigationRouteLike | undefined): ActiveWorkspaceSelection | null {
if (!route) {
return null;
}
if (typeof route.path === "string") {
const parsed = parseHostWorkspaceRouteFromPathname(route.path);
if (parsed) {
return parsed;
}
}
const params =
route.params && typeof route.params === "object"
? (route.params as {
serverId?: string | string[];
workspaceId?: string | string[];
})
: null;
const serverValue = Array.isArray(params?.serverId) ? params?.serverId[0] : params?.serverId;
const workspaceValue = Array.isArray(params?.workspaceId)
? params?.workspaceId[0]
: params?.workspaceId;
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
const workspaceId =
typeof workspaceValue === "string"
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "")
: "";
if (!serverId || !workspaceId) {
return null;
}
return { serverId, workspaceId };
}
export function syncNavigationActiveWorkspace(
navigationRef: NavigationObserverRef
) {
emitIfChanged(
extractActiveWorkspaceFromRoute(navigationRef.current?.getCurrentRoute() as NavigationRouteLike | undefined)
);
}
export function useNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}