diff --git a/plugins/tasks/shell/app-shell.tsx b/plugins/tasks/shell/app-shell.tsx
index 365038ea08..61ae5e838a 100644
--- a/plugins/tasks/shell/app-shell.tsx
+++ b/plugins/tasks/shell/app-shell.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState, type ReactNode } from "react";
+import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
 import type { PluginNavPanelProps } from "@get-bb/plugin-sdk/app";
 import {
   useActiveTasks,
@@ -10,8 +10,11 @@ import {
 import {
   parseTasksRoute,
   useTasksNavigation,
+  type ResolvedTasksRoute,
+  type TasksNavigation,
   type TasksRoute,
 } from "./routes.js";
+import { loadViewMode, storeViewMode } from "./view-preference.js";
 import { TasksSidebar } from "./sidebar.js";
 import {
   loadSidebarCollapsed,
@@ -154,7 +157,7 @@ function RouteOutlet({
   route,
   boardUsable,
 }: {
-  route: TasksRoute;
+  route: ResolvedTasksRoute;
   /** False in phone-width containers: board routes fall back to the list
       (deep links/rotation would otherwise strand a crushed board with the
       toggle hidden). The URL keeps the board view for when width returns. */
@@ -178,9 +181,31 @@ function RouteOutlet({
   }
 }
 
+/**
+ * A project URL without a `?view=` marker (sidebar click, breadcrumb, deep
+ * link) restores the view this client last used for that project.
+ */
+function resolveRoute(route: TasksRoute): ResolvedTasksRoute {
+  if (route.kind !== "project") return route;
+  return { ...route, view: route.view ?? loadViewMode(route.projectId) };
+}
+
 function TasksAppShellContent({ subPath }: PluginNavPanelProps) {
-  const route = parseTasksRoute(subPath);
-  const navigation = useTasksNavigation();
+  const route = resolveRoute(parseTasksRoute(subPath));
+  const tasksNavigation = useTasksNavigation();
+  // Every explicit project view in a navigation is a user choice worth
+  // remembering — the topbar's List/Board toggle is the only source of one.
+  const navigation = useMemo<TasksNavigation>(
+    () => ({
+      go: (target, options) => {
+        if (target.kind === "project" && target.view !== null) {
+          storeViewMode(target.projectId, target.view);
+        }
+        tasksNavigation.go(target, options);
+      },
+    }),
+    [tasksNavigation],
+  );
   const [sidebarCollapsed, setSidebarCollapsed] =
     useState(loadSidebarCollapsed);
   const [newTaskOpen, setNewTaskOpen] = useState(false);
diff --git a/plugins/tasks/shell/routes.ts b/plugins/tasks/shell/routes.ts
index 46cf4b7143..74b7e7992c 100644
--- a/plugins/tasks/shell/routes.ts
+++ b/plugins/tasks/shell/routes.ts
@@ -6,13 +6,23 @@ export const PANEL_PATH = "tasks";
 
 export type TaskViewMode = "list" | "board";
 
+/**
+ * A project route's `view` is `null` when the URL names no view — the shell
+ * then resolves the user's stored preference for that project (see
+ * view-preference.ts). Navigating with an explicit view pins it in the URL.
+ */
 export type TasksRoute =
   | { kind: "all" }
   | { kind: "active" }
   | { kind: "manage" }
-  | { kind: "project"; projectId: string; view: TaskViewMode }
+  | { kind: "project"; projectId: string; view: TaskViewMode | null }
   | { kind: "task"; taskKey: string };
 
+/** A route whose project view has been resolved; what the shell renders. */
+export type ResolvedTasksRoute =
+  | Exclude<TasksRoute, { kind: "project" }>
+  | { kind: "project"; projectId: string; view: TaskViewMode };
+
 /**
  * subPath grammar (the trailing route below /plugins/tasks/tasks):
  *   ""                      → all tasks (default)
@@ -20,7 +30,8 @@ export type TasksRoute =
  *   "active"                → tasks with agents working
  *   "manage"                → manage panel (labels, presets, folders)
  *   "task/<taskKey>"        → task detail (e.g. task/TSK-4)
- *   "<projectId>"           → project list view
+ *   "<projectId>"           → project, view from the stored preference
+ *   "<projectId>?view=list"  → project list view
  *   "<projectId>?view=board" → project board view
  */
 function decodeSegment(segment: string): string {
@@ -52,7 +63,9 @@ export function parseTasksRoute(rawSubPath: string): TasksRoute {
   return {
     kind: "project",
     projectId: head,
-    view: view === "board" ? "board" : "list",
+    // Anything other than the two known views (including no marker at all)
+    // leaves the choice to the caller's stored preference.
+    view: view === "board" || view === "list" ? view : null,
   };
 }
 
@@ -67,9 +80,9 @@ export function tasksRouteToSubPath(route: TasksRoute): string {
     case "task":
       return `task/${route.taskKey}`;
     case "project":
-      return route.view === "board"
-        ? `${route.projectId}?view=board`
-        : route.projectId;
+      return route.view === null
+        ? route.projectId
+        : `${route.projectId}?view=${route.view}`;
   }
 }
 
diff --git a/plugins/tasks/shell/shell.test.tsx b/plugins/tasks/shell/shell.test.tsx
index 4c33cf3de3..6a30318a76 100644
--- a/plugins/tasks/shell/shell.test.tsx
+++ b/plugins/tasks/shell/shell.test.tsx
@@ -24,6 +24,7 @@ const { parseTasksRoute, tasksRouteToSubPath } = await import("./routes.js");
 const { pagerPosition } = await import("./topbar.js");
 const { SIDEBAR_COLLAPSED_STORAGE_KEY } =
   await import("./sidebar-preference.js");
+const { loadViewMode } = await import("./view-preference.js");
 
 beforeEach(() => window.localStorage.clear());
 afterEach(() => {
@@ -32,6 +33,7 @@ afterEach(() => {
 });
 
 const PROJECT_ID = "01HZZZZZZZZZZZZZZZZZZZZZP1";
+const OTHER_PROJECT_ID = "01HZZZZZZZZZZZZZZZZZZZZZP2";
 const FOLDER_ID = "01HZZZZZZZZZZZZZZZZZZZZZF1";
 
 const project = {
@@ -81,6 +83,8 @@ describe("tasks route grammar", () => {
       { kind: "task", taskKey: "TSK-4" },
       { kind: "project", projectId: PROJECT_ID, view: "list" },
       { kind: "project", projectId: PROJECT_ID, view: "board" },
+      // No view marker: the shell fills it from the stored preference.
+      { kind: "project", projectId: PROJECT_ID, view: null },
     ] as const;
     for (const route of routes) {
       expect(parseTasksRoute(tasksRouteToSubPath(route))).toEqual(route);
@@ -92,6 +96,82 @@ describe("tasks route grammar", () => {
       view: "board",
     });
     expect(parseTasksRoute("")).toEqual({ kind: "all" });
+    // An unknown marker is as good as none — never a silent "list".
+    expect(parseTasksRoute(`${PROJECT_ID}?view=kanban`)).toEqual({
+      kind: "project",
+      projectId: PROJECT_ID,
+      view: null,
+    });
+  });
+});
+
+describe("project view preference", () => {
+  const openProject = (subPath: string) =>
+    renderSlot(
+      app.navPanels[0]!,
+      { subPath },
+      { rpc: seededRpc({ listLabels: () => ({ labels: [] }) }) },
+    );
+
+  it("restores the remembered view when the URL names none", async () => {
+    const listed = openProject(`${PROJECT_ID}?view=list`);
+    // The toggle is the only way a user picks a view; it must persist.
+    fireEvent.click(await listed.findByRole("button", { name: "Board" }));
+    expect(listed.navigateCalls).toContainEqual({
+      method: "toPluginPanel",
+      path: "tasks",
+      options: { subPath: `${PROJECT_ID}?view=board` },
+    });
+    listed.lifecycle.unmount();
+
+    // Reopening the project without a marker (sidebar click, deep link).
+    const reopened = openProject(PROJECT_ID);
+    const boardSegment = await reopened.findByRole("button", { name: "Board" });
+    expect(boardSegment.getAttribute("aria-pressed")).toBe("true");
+    await reopened.findByText("In Review");
+  });
+
+  it("keeps per-project choices apart and defaults unseen projects to the last one used", async () => {
+    const slot = openProject(`${PROJECT_ID}?view=list`);
+    fireEvent.click(await slot.findByRole("button", { name: "Board" }));
+    slot.lifecycle.unmount();
+
+    expect(loadViewMode(PROJECT_ID)).toBe("board");
+    // A project opened for the first time follows the most recent choice
+    // rather than snapping back to the list.
+    expect(loadViewMode(OTHER_PROJECT_ID)).toBe("board");
+
+    const other = renderSlot(
+      app.navPanels[0]!,
+      { subPath: `${OTHER_PROJECT_ID}?view=list` },
+      { rpc: seededRpc() },
+    );
+    fireEvent.click(await other.findByRole("button", { name: "List" }));
+    expect(loadViewMode(OTHER_PROJECT_ID)).toBe("list");
+    expect(loadViewMode(PROJECT_ID)).toBe("board");
+  });
+
+  it("navigates from the sidebar without pinning a view", async () => {
+    const slot = openProject("all");
+    fireEvent.click(await slot.findByText("Tasks Plugin"));
+    expect(slot.navigateCalls).toContainEqual({
+      method: "toPluginPanel",
+      path: "tasks",
+      options: { subPath: PROJECT_ID },
+    });
+  });
+
+  it("still toggles when client storage rejects writes", async () => {
+    vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
+      throw new DOMException("Storage is disabled", "SecurityError");
+    });
+    const slot = openProject(`${PROJECT_ID}?view=list`);
+    fireEvent.click(await slot.findByRole("button", { name: "Board" }));
+    expect(slot.navigateCalls).toContainEqual({
+      method: "toPluginPanel",
+      path: "tasks",
+      options: { subPath: `${PROJECT_ID}?view=board` },
+    });
   });
 });
 
diff --git a/plugins/tasks/shell/sidebar.tsx b/plugins/tasks/shell/sidebar.tsx
index ce77593d72..01e38f9c36 100644
--- a/plugins/tasks/shell/sidebar.tsx
+++ b/plugins/tasks/shell/sidebar.tsx
@@ -262,12 +262,9 @@ export function TasksSidebar({
     [summaries],
   );
   const activeProjectId = route.kind === "project" ? route.projectId : null;
+  // No explicit view: the shell restores the view last used for that project.
   const openProject = (projectId: string) =>
-    onNavigate({
-      kind: "project",
-      projectId,
-      view: route.kind === "project" ? route.view : "list",
-    });
+    onNavigate({ kind: "project", projectId, view: null });
   const toggleFolder = (folderId: string) =>
     setCollapsedFolders((current) => {
       const next = new Set(current);
diff --git a/plugins/tasks/shell/topbar.tsx b/plugins/tasks/shell/topbar.tsx
index e5a9b81c07..a7b053ab96 100644
--- a/plugins/tasks/shell/topbar.tsx
+++ b/plugins/tasks/shell/topbar.tsx
@@ -2,7 +2,11 @@ import { useCallback, useMemo } from "react";
 import type { Project, Task } from "../shared/contract.js";
 import { groupTasksByStatus } from "../views/list/lib.js";
 import { listAllTasks, useTasksQuery } from "./data.js";
-import type { TaskViewMode, TasksRoute } from "./routes.js";
+import type {
+  ResolvedTasksRoute,
+  TaskViewMode,
+  TasksRoute,
+} from "./routes.js";
 import { Button } from "@bb/shared-ui/button";
 import { Icon } from "@bb/shared-ui/icon";
 import { cn } from "@bb/shared-ui/lib/utils";
@@ -182,7 +186,7 @@ function RefreshTasksButton() {
 }
 
 export interface TasksTopbarProps {
-  route: TasksRoute;
+  route: ResolvedTasksRoute;
   projects: Project[] | undefined;
   sidebarCollapsed: boolean;
   /**
@@ -277,10 +281,12 @@ export function TasksTopbar({
                 type="button"
                 className="hidden min-w-0 items-center gap-2 text-muted-foreground hover:text-foreground @md:flex"
                 onClick={() =>
+                  // No explicit view: the shell restores the project's
+                  // remembered List/Board choice.
                   onNavigate({
                     kind: "project",
                     projectId: project.id,
-                    view: "list",
+                    view: null,
                   })
                 }
               >
diff --git a/plugins/tasks/shell/view-preference.test.ts b/plugins/tasks/shell/view-preference.test.ts
new file mode 100644
index 0000000000..58ab6a3b8a
--- /dev/null
+++ b/plugins/tasks/shell/view-preference.test.ts
@@ -0,0 +1,59 @@
+// @vitest-environment jsdom
+import { beforeEach, describe, expect, it } from "vitest";
+import {
+  VIEW_PREFERENCE_STORAGE_KEY,
+  VIEW_PREFERENCE_VERSION,
+  loadViewMode,
+  storeViewMode,
+} from "./view-preference.js";
+
+const PROJECT_A = "01HZZZZZZZZZZZZZZZZZZZZZP1";
+const PROJECT_B = "01HZZZZZZZZZZZZZZZZZZZZZP2";
+
+beforeEach(() => window.localStorage.clear());
+
+describe("view preference storage", () => {
+  it("falls back to the list before anything is stored", () => {
+    expect(loadViewMode(PROJECT_A)).toBe("list");
+  });
+
+  it("keeps other projects' choices when one project changes", () => {
+    storeViewMode(PROJECT_A, "board");
+    storeViewMode(PROJECT_B, "list");
+    expect(loadViewMode(PROJECT_A)).toBe("board");
+    expect(loadViewMode(PROJECT_B)).toBe("list");
+  });
+
+  it("treats corrupt or partial documents as unset rather than throwing", () => {
+    window.localStorage.setItem(VIEW_PREFERENCE_STORAGE_KEY, "{not json");
+    expect(loadViewMode(PROJECT_A)).toBe("list");
+
+    window.localStorage.setItem(
+      VIEW_PREFERENCE_STORAGE_KEY,
+      JSON.stringify({
+        version: VIEW_PREFERENCE_VERSION,
+        lastUsed: "kanban",
+        projects: { [PROJECT_A]: "gallery" },
+      }),
+    );
+    expect(loadViewMode(PROJECT_A)).toBe("list");
+  });
+
+  it("leaves a document written by a newer client untouched", () => {
+    const future = JSON.stringify({
+      version: VIEW_PREFERENCE_VERSION + 1,
+      lastUsed: "board",
+      projects: { [PROJECT_A]: "board" },
+      timeline: "view a future build added",
+    });
+    window.localStorage.setItem(VIEW_PREFERENCE_STORAGE_KEY, future);
+
+    // Known fields still read; the write is refused so the future build's own
+    // fields survive this session.
+    expect(loadViewMode(PROJECT_A)).toBe("board");
+    storeViewMode(PROJECT_A, "list");
+    expect(window.localStorage.getItem(VIEW_PREFERENCE_STORAGE_KEY)).toBe(
+      future,
+    );
+  });
+});
diff --git a/plugins/tasks/shell/view-preference.ts b/plugins/tasks/shell/view-preference.ts
new file mode 100644
index 0000000000..a85a80b540
--- /dev/null
+++ b/plugins/tasks/shell/view-preference.ts
@@ -0,0 +1,108 @@
+import type { TaskViewMode } from "./routes.js";
+
+/**
+ * Client-local List/Board choice per project. Stored in the browser profile so
+ * one client does not rewrite another client connected to the same bb server —
+ * the same boundary as the sidebar and list preferences.
+ *
+ * A project route without an explicit `?view=` resolves through here, so
+ * reopening a project restores the view the user last picked for it. Projects
+ * never opened before fall back to the last view chosen anywhere, then to the
+ * list.
+ */
+export const VIEW_PREFERENCE_STORAGE_KEY = "bb-tasks:view-preferences";
+export const VIEW_PREFERENCE_VERSION = 1 as const;
+
+export const DEFAULT_VIEW_MODE: TaskViewMode = "list";
+
+interface StoredDocumentV1 {
+  version: typeof VIEW_PREFERENCE_VERSION;
+  /** View chosen most recently on any project; default for unseen projects. */
+  lastUsed: TaskViewMode;
+  projects: Record<string, TaskViewMode>;
+}
+
+function asViewMode(value: unknown): TaskViewMode | null {
+  return value === "list" || value === "board" ? value : null;
+}
+
+interface ParsedStorage {
+  lastUsed: TaskViewMode | null;
+  projects: Record<string, unknown>;
+  /** True when the document was written by a newer client. */
+  isFutureVersion: boolean;
+}
+
+function readStorage(): ParsedStorage | null {
+  try {
+    const raw = window.localStorage.getItem(VIEW_PREFERENCE_STORAGE_KEY);
+    if (raw === null) return null;
+    const parsed: unknown = JSON.parse(raw);
+    if (
+      parsed === null ||
+      typeof parsed !== "object" ||
+      Array.isArray(parsed)
+    ) {
+      return null;
+    }
+    const record = parsed as Record<string, unknown>;
+    const version =
+      typeof record.version === "number" && Number.isFinite(record.version)
+        ? record.version
+        : null;
+    // No older versions shipped; refuse rather than invent fields.
+    if (version !== null && version < VIEW_PREFERENCE_VERSION) return null;
+    const projects =
+      record.projects !== null &&
+      typeof record.projects === "object" &&
+      !Array.isArray(record.projects)
+        ? (record.projects as Record<string, unknown>)
+        : {};
+    return {
+      lastUsed: asViewMode(record.lastUsed),
+      projects,
+      isFutureVersion: version !== null && version > VIEW_PREFERENCE_VERSION,
+    };
+  } catch {
+    return null;
+  }
+}
+
+export function loadViewMode(projectId: string): TaskViewMode {
+  const document = readStorage();
+  if (document === null) return DEFAULT_VIEW_MODE;
+  return (
+    asViewMode(document.projects[projectId]) ??
+    document.lastUsed ??
+    DEFAULT_VIEW_MODE
+  );
+}
+
+/**
+ * Persist the view for one project and make it the fallback for projects the
+ * user has not opened yet. Refuses to overwrite storage written by a newer
+ * client so older builds cannot down-convert a future document.
+ */
+export function storeViewMode(projectId: string, view: TaskViewMode): void {
+  try {
+    const existing = readStorage();
+    if (existing?.isFutureVersion) return;
+    const projects: Record<string, TaskViewMode> = {};
+    for (const [id, value] of Object.entries(existing?.projects ?? {})) {
+      const mode = asViewMode(value);
+      if (mode !== null) projects[id] = mode;
+    }
+    projects[projectId] = view;
+    const document: StoredDocumentV1 = {
+      version: VIEW_PREFERENCE_VERSION,
+      lastUsed: view,
+      projects,
+    };
+    window.localStorage.setItem(
+      VIEW_PREFERENCE_STORAGE_KEY,
+      JSON.stringify(document),
+    );
+  } catch {
+    // Persistence is best-effort (private mode / storage disabled).
+  }
+}
diff --git a/plugins/tasks/views/manage/new-project-dialog.tsx b/plugins/tasks/views/manage/new-project-dialog.tsx
index 6d014f5108..f94c3cba35 100644
--- a/plugins/tasks/views/manage/new-project-dialog.tsx
+++ b/plugins/tasks/views/manage/new-project-dialog.tsx
@@ -150,7 +150,8 @@ export function NewProjectDialog({ open, onOpenChange }: NewProjectDialogProps)
         linkedBbProjectId: linkedTrimmed === "" ? null : linkedTrimmed,
       });
       onOpenChange(false);
-      navigation.go({ kind: "project", projectId: project.id, view: "list" });
+      // No explicit view: the shell opens the view this client last used.
+      navigation.go({ kind: "project", projectId: project.id, view: null });
     } catch (submitError) {
       setError(describeCreateProjectError(submitError));
     } finally {
