From 428ef707adc5c3c7e0396c19d21569b2e48ce5b6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 26 Oct 2026 15:46:10 +0700 Subject: [PATCH] fix(app): demand the host directory from the workspace route (#7918) A workspace restored at launch with no open chat had nothing demanding its host's directory on a phone, so the deck waited on hydration that never started. The workspace route now keeps its host's directory demanded while mounted, through the same hook the agent route uses. --- .../src/app/h/[serverId]/agent/[agentId].tsx | 7 +--- .../workspace/[workspaceId]/index.tsx | 5 +++ .../src/hooks/use-directory-demand.test.tsx | 40 +++++++++++++++++++ .../app/src/hooks/use-directory-demand.ts | 16 ++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 packages/app/src/hooks/use-directory-demand.test.tsx create mode 100644 packages/app/src/hooks/use-directory-demand.ts diff --git a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx index 008999a540..551e3b604d 100644 --- a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx +++ b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx @@ -5,6 +5,7 @@ import { useFetchQuery } from "@/data/query"; import { resolveAgentRoute, type AgentRouteLookup } from "@/navigation/agent-route-resolution"; import { AgentRouteResolutionView } from "@/navigation/agent-route-resolution-view"; import { useHasHydratedWorkspaces } from "@/stores/session-store-hooks"; +import { useDirectoryDemand } from "@/hooks/use-directory-demand"; import { useWorkspaceLayoutStoreHydrated } from "@/stores/workspace-layout-store"; import { useSessionStore } from "@/stores/session-store"; import { getHostRuntimeStore, useHostRuntimeSnapshot, useHosts } from "@/runtime/host-runtime"; @@ -87,11 +88,7 @@ function HostAgentReadyRouteContent() { ]); const hasHydratedWorkspaces = useHasHydratedWorkspaces(serverId); const hasHydratedLayout = useWorkspaceLayoutStoreHydrated(); - useEffect(() => { - if (!serverId) return; - // A compact app can open this route before its sidebar has demanded the directory. - return getHostRuntimeStore().acquireDirectoryDemand(serverId); - }, [serverId]); + useDirectoryDemand(serverId); const resolution = resolveAgentRoute({ hasHydratedWorkspaces, hasHydratedLayout, diff --git a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/index.tsx b/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/index.tsx index 8f80f185c7..2ea41196aa 100644 --- a/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/index.tsx +++ b/packages/app/src/app/h/[serverId]/workspace/[workspaceId]/index.tsx @@ -9,6 +9,7 @@ import { useActiveWorkspaceSelection, } from "@/stores/navigation-active-workspace-store"; import { useHasHydratedWorkspaces, useWorkspaceExists } from "@/stores/session-store-hooks"; +import { useDirectoryDemand } from "@/hooks/use-directory-demand"; import type { WorkspaceTabTarget } from "@/workspace-tabs/model"; import { WorkspaceScreen } from "@/screens/workspace/workspace-screen"; import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; @@ -110,6 +111,10 @@ function HostWorkspaceRouteContent() { ? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "") : ""; const openValue = getParamValue(params.open); + // A restored workspace with no open chat has nothing else demanding its host's directory on a + // phone, where the sidebar only demands while its panel is active. Without demand the deck + // waits on workspace hydration that never starts. + useDirectoryDemand(serverId); useEffect(() => { if (!serverId || !workspaceId) { return; diff --git a/packages/app/src/hooks/use-directory-demand.test.tsx b/packages/app/src/hooks/use-directory-demand.test.tsx new file mode 100644 index 0000000000..7a03d83471 --- /dev/null +++ b/packages/app/src/hooks/use-directory-demand.test.tsx @@ -0,0 +1,40 @@ +/** + * @vitest-environment jsdom + */ +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getHostRuntimeStore } from "@/runtime/host-runtime"; +import { useDirectoryDemand } from "./use-directory-demand"; + +function Probe({ serverId }: { serverId: string | null }) { + useDirectoryDemand(serverId); + return null; +} + +describe("useDirectoryDemand", () => { + let root: Root | null = null; + afterEach(() => { + act(() => root?.unmount()); + root = null; + vi.restoreAllMocks(); + }); + + it("demands the host directory while mounted and releases it on unmount", () => { + const release = vi.fn(); + const acquire = vi + .spyOn(getHostRuntimeStore(), "acquireDirectoryDemand") + .mockImplementation(() => release); + const container = document.createElement("div"); + root = createRoot(container); + + act(() => root?.render()); + expect(acquire).toHaveBeenCalledTimes(1); + expect(acquire).toHaveBeenCalledWith("srv_a"); + expect(release).not.toHaveBeenCalled(); + + act(() => root?.render()); + expect(release).toHaveBeenCalledTimes(1); + expect(acquire).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/app/src/hooks/use-directory-demand.ts b/packages/app/src/hooks/use-directory-demand.ts new file mode 100644 index 0000000000..f4d2953ab0 --- /dev/null +++ b/packages/app/src/hooks/use-directory-demand.ts @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import { getHostRuntimeStore } from "@/runtime/host-runtime"; + +/** + * Keeps a host's directory demanded while the caller is mounted. + * + * The sidebar only demands the directory while its panel is active, so a compact app that + * restores straight into a workspace or agent route has nothing else asking the host for + * workspaces and agents. Without demand the route waits on hydration that never starts. + */ +export function useDirectoryDemand(serverId: string | null | undefined): void { + useEffect(() => { + if (!serverId) return; + return getHostRuntimeStore().acquireDirectoryDemand(serverId); + }, [serverId]); +}