diff --git a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
index 792932239..7ce87d84d 100644
--- a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
@@ -1,6 +1,6 @@
 // @vitest-environment jsdom
 
-import { cleanup, render, waitFor } from "@testing-library/react";
+import { act, cleanup, render, waitFor } from "@testing-library/react";
 import type { ReactNode } from "react";
 import { MemoryRouter } from "react-router-dom";
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -10,6 +10,15 @@ const ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY = "bb.root-compose.project-id";
 
 const mockUseThread = vi.hoisted(() => vi.fn());
 const mockUseThreadDetailBootstrap = vi.hoisted(() => vi.fn());
+const commandHandlers = vi.hoisted(() => new Map<string, () => boolean>());
+
+vi.mock("@/components/commands/AppCommandProvider", () => ({
+  useAppCommandHandler: (command: string, handler: () => boolean) => {
+    commandHandlers.set(command, handler);
+  },
+  useAppCommandShortcut: () => null,
+  useIsAppCommandModifierHeld: () => false,
+}));
 
 vi.mock("@/components/sidebar/AppSidebar", () => ({
   AppSidebar: () => <aside data-testid="app-sidebar" />,
@@ -145,6 +154,7 @@ vi.mock("@/hooks/queries/thread-queries", () => ({
 describe("AppLayout root compose project preference", () => {
   beforeEach(() => {
     window.localStorage.clear();
+    commandHandlers.clear();
     mockUseThread.mockReturnValue({
       data: {
         id: "thr_opened",
@@ -164,10 +174,11 @@ describe("AppLayout root compose project preference", () => {
   afterEach(() => {
     cleanup();
     window.localStorage.clear();
+    commandHandlers.clear();
     vi.clearAllMocks();
   });
 
-  it("does not replace the new-thread project preference with the opened thread project", async () => {
+  it("uses the opened thread project for the new-thread command", async () => {
     window.localStorage.setItem(
       ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY,
       "proj_last_run",
@@ -187,6 +198,35 @@ describe("AppLayout root compose project preference", () => {
       expect(document.title).toBe("Opened Thread");
     });
 
+    act(() => {
+      expect(commandHandlers.get("thread.new")?.()).toBe(true);
+    });
+
+    await waitFor(() => {
+      expect(
+        window.localStorage.getItem(ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY),
+      ).toBe("proj_opened");
+    });
+  });
+
+  it("keeps the stored project when the route has no project", () => {
+    window.localStorage.setItem(
+      ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY,
+      "proj_last_run",
+    );
+
+    render(
+      <MemoryRouter initialEntries={["/"]}>
+        <AppLayout>
+          <div>New thread route</div>
+        </AppLayout>
+      </MemoryRouter>,
+    );
+
+    act(() => {
+      expect(commandHandlers.get("thread.new")?.()).toBe(true);
+    });
+
     expect(
       window.localStorage.getItem(ROOT_COMPOSE_PROJECT_ID_STORAGE_KEY),
     ).toBe("proj_last_run");
diff --git a/apps/app/src/components/layout/AppLayout.tsx b/apps/app/src/components/layout/AppLayout.tsx
index 3e90688d1..4f99a66fd 100644
--- a/apps/app/src/components/layout/AppLayout.tsx
+++ b/apps/app/src/components/layout/AppLayout.tsx
@@ -93,6 +93,7 @@ import { applyThreadOpenToLayout } from "@/views/thread-detail/splitThreadNaviga
 import { useThreadSplitsEnabled } from "@/hooks/useThreadSplitsEnabled";
 import { useSplitWorkspaceActive } from "@/hooks/useSplitWorkspaceActive";
 import { useAppSettingsRouteMemory } from "@/hooks/useAppSettingsRouteMemory";
+import { useSetRootComposeProjectId } from "@/lib/root-compose-selection";
 
 const SIDEBAR_WIDTH_KEY = "bb.sidebar.width";
 const SIDEBAR_OPEN_KEY = "bb.sidebar.open";
@@ -416,6 +417,14 @@ export function AppLayout({ children }: AppLayoutProps) {
     restoreIOSViewportOnKeyboardDismissal,
   );
   const location = useLocation();
+  const {
+    projectId,
+    threadId,
+    isThreadView,
+    isArchivedView,
+    isSettingsView,
+    isRootView,
+  } = useRouteState();
   const [resourceRouteLabel, setResourceRouteLabel] = useAtom(
     resourceRouteLabelAtom,
   );
@@ -452,6 +461,7 @@ export function AppLayout({ children }: AppLayoutProps) {
     toolsBackRoutePath,
     toolsRoutePath,
   } = useAppSettingsRouteMemory();
+  const setRootComposeProjectId = useSetRootComposeProjectId();
   useEffect(
     () =>
       wsManager.onThreadOpen((signal) => {
@@ -481,6 +491,9 @@ export function AppLayout({ children }: AppLayoutProps) {
     [isCompactViewport, navigate, store, threadSplitsEnabled],
   );
   useAppCommandHandler("thread.new", () => {
+    if (projectId !== undefined) {
+      setRootComposeProjectId(projectId);
+    }
     void navigate(getRootComposeRoutePath(), {
       state: { focusPrompt: true },
     });
@@ -495,14 +508,6 @@ export function AppLayout({ children }: AppLayoutProps) {
     void navigate(`${SETTINGS_ROUTE_PATH}/servers`);
     return true;
   });
-  const {
-    projectId,
-    threadId,
-    isThreadView,
-    isArchivedView,
-    isSettingsView,
-    isRootView,
-  } = useRouteState();
   const archivedSectionId = isArchivedView
     ? new URLSearchParams(location.search).get("sectionId")
     : null;
