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]);
+}