diff --git a/doc/PRODUCT.md b/doc/PRODUCT.md
index dc54b088d8a..70aa5810afc 100644
--- a/doc/PRODUCT.md
+++ b/doc/PRODUCT.md
@@ -148,6 +148,12 @@ submits.
### Task collection creation filters
+Tasks opens All tasks for new users. A saved view or an explicit view in the URL
+takes precedence over this default.
+The task table orders rows by Last updated, newest first, without highlighting a
+column header. Selecting a column sort activates its header and direction arrow;
+explicit sort URLs retain that selection.
+
The task Filters menu also offers **Routine tasks**. It selects tasks with
`originKind=routine_execution`, including manually triggered routine runs, even
when other automatic tasks are hidden. The routines list's **View all tasks**
@@ -429,7 +435,7 @@ Coordinator navigation uses **Chat**, **Tasks**, and **Inbox**. Inbox uses the
inbox icon and keeps the existing decision feed, its count, and decision history
at `/decisions`. The previous Inbox destination under More is removed.
-Tasks defaults to **Mine**, with a saved view per person and company. Mine uses
+Tasks defaults to **All tasks**, with a saved view per person and company. Mine uses
the previous Inbox's personal participation rules, mixed attention items,
read/unread title weights, keyboard shortcuts, archive and undo, mobile
swipe-to-archive, grouping, and saved columns. Its existing preferences carry
@@ -443,7 +449,7 @@ In the Classic workspace, an opt-in setting (`enableCombinedInboxTasks`, off by
folds Inbox into Tasks. The Inbox nav row goes away and its unread badge moves
to Tasks. A Views menu on the task list covers the inbox views (Mine, Unread,
Blocked, Recent, Everything) and the task-status views (All, Active, Backlog,
-Done). Bare `/issues` opens the last-used view, defaulting to Mine; links that
+Done). Bare `/issues` opens the last-used view, defaulting to All tasks; links that
carry a task filter open All. Old `/inbox` links redirect to the matching view.
### CSV file previews
diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md
index a504d078707..44efe30d8e9 100644
--- a/doc/SPEC-implementation.md
+++ b/doc/SPEC-implementation.md
@@ -2846,6 +2846,11 @@ precedence and dependency failures use the original built-in detector.
### Personal workspace preference
+Tasks defaults to All tasks when the viewer has no saved task view. Explicit
+view URLs and existing saved task views retain precedence.
+Task tables default to Last updated descending without an active sort header;
+an explicit column sort selects the header and shows its direction.
+
Coordinator is the standard workspace for every person. It is not an experiment.
Settings → Experimental offers **Classic workspace**, a personal opt-in that
defaults off. Existing Coordinator experiment values, including historical off
diff --git a/ui/src/components/TaskList.test.tsx b/ui/src/components/TaskList.test.tsx
index f7388a29a43..25ddf723a19 100644
--- a/ui/src/components/TaskList.test.tsx
+++ b/ui/src/components/TaskList.test.tsx
@@ -34,7 +34,28 @@ describe("TaskList progressive rendering", () => {
];
await act(async () => root.render());
expect([...rows()].map(row => row.getAttribute("data-issue-row-details"))).toEqual(["newest-root", "parent", "child-newest", "child-older"]);
- expect(host.querySelector('[role="columnheader"][aria-sort="descending"]')?.textContent).toContain("Last updated");
+ expect(host.querySelector('[role="columnheader"][aria-sort="descending"]')).toBeNull();
+ expect(host.querySelector('.task-list-sort-button[data-active]')).toBeNull();
+ expect(host.querySelector('[data-slot="task-list-sort-direction"]')).toBeNull();
+ });
+ it("highlights Last updated only after selection and returns to the unhighlighted default when reset", async () => {
+ const items = tasks(2).reverse();
+ await render(items);
+ const orderedIds = () => [...rows()].map(row => row.getAttribute("data-issue-row-details"));
+ expect(orderedIds()).toEqual(["task-0", "task-1"]);
+ await act(async () => host.querySelector('button[aria-label="Last updated: sort ascending"]')!.click());
+ expect(orderedIds()).toEqual(["task-1", "task-0"]);
+ expect(host.querySelector('.task-list-sort-button[data-active]')?.textContent).toBe("Last updated");
+ expect(host.querySelector('[data-column="lastUpdated"][aria-sort="ascending"]')).not.toBeNull();
+ await act(async () => host.querySelector('button[aria-label="Last updated: sort descending"]')!.click());
+ expect(orderedIds()).toEqual(["task-0", "task-1"]);
+ expect(host.querySelector('[data-column="lastUpdated"][aria-sort="descending"]')).not.toBeNull();
+ await act(async () => host.querySelector('button[aria-label="Columns"]')!.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true })));
+ const reset = [...document.body.querySelectorAll('[role="menuitem"]')].find(item => item.textContent === "Reset defaults");
+ expect(reset).toBeDefined();
+ await act(async () => reset!.click());
+ expect(orderedIds()).toEqual(["task-0", "task-1"]);
+ expect(host.querySelector('.task-list-sort-button[data-active]')).toBeNull();
});
it("honors a creation-time URL sort and reports column sorting", async () => {
const items = [
@@ -50,6 +71,7 @@ describe("TaskList progressive rendering", () => {
await act(async () => root.render());
expect([...rows()].map(row => row.getAttribute("data-issue-row-details"))).toEqual(["older", "newer"]);
+ expect(host.querySelector('[data-column="taskId"][aria-sort="ascending"]')).not.toBeNull();
await act(async () => host.querySelector('button[aria-label="Task ID: sort descending"]')!.click());
expect(onSortChange).toHaveBeenLastCalledWith({ column: "taskId", direction: "descending" });
});
diff --git a/ui/src/components/TaskList.tsx b/ui/src/components/TaskList.tsx
index 73e4c168cde..b653de661b9 100644
--- a/ui/src/components/TaskList.tsx
+++ b/ui/src/components/TaskList.tsx
@@ -59,7 +59,7 @@ export function TaskList({ items, allItems = items, initialColumns = defaultTask
const localKey = useId();
const { columns, order: columnOrder, setColumns, setOrder: setColumnOrder } = useTaskListColumnPreferences(preferenceLocation, initialColumns);
const [localSort, setLocalSort] = useState();
- const sort = controlledSort ?? (onSortChange ? undefined : localSort) ?? defaultTaskListSort;
+ const sort = controlledSort ?? (onSortChange ? undefined : localSort);
const setSort = (next: TaskListSort | undefined) => {
setLocalSort(next);
onSortChange?.(next);
diff --git a/ui/src/lib/task-list-views.test.ts b/ui/src/lib/task-list-views.test.ts
index e227b579f82..4eb6841d3ba 100644
--- a/ui/src/lib/task-list-views.test.ts
+++ b/ui/src/lib/task-list-views.test.ts
@@ -46,7 +46,7 @@ describe("task list views", () => {
expect(loadTaskListView("key", denied)).toBe("all");
expect(() => saveTaskListView("key", "inbox", denied)).not.toThrow();
});
- it("uses the coordinator's Inbox default for missing, invalid or denied preferences, preserving saved choices", () => {
+ it("honors an explicit fallback for missing, invalid or denied preferences, preserving saved choices", () => {
for (const value of [null, "invalid", "all", "unread", "blocked", "inbox"]) {
const storage = { getItem: () => value, setItem: () => {} };
expect(loadTaskListView("key", storage, "inbox")).toBe(value === null || value === "invalid" ? "inbox" : value);
diff --git a/ui/src/lib/task-views.test.ts b/ui/src/lib/task-views.test.ts
index 1dd594b86b3..3c27c86d796 100644
--- a/ui/src/lib/task-views.test.ts
+++ b/ui/src/lib/task-views.test.ts
@@ -79,7 +79,7 @@ describe("resolveInitialTaskView", () => {
it("falls back to the last-used view on a bare /issues", () => {
expect(resolveInitialTaskView(null, false, "backlog")).toBe("backlog");
expect(resolveInitialTaskView("nonsense", false, "backlog")).toBe("backlog");
- expect(resolveInitialTaskView(null, false, DEFAULT_TASK_VIEW)).toBe("mine");
+ expect(resolveInitialTaskView(null, false, DEFAULT_TASK_VIEW)).toBe("all");
});
it("opens All tasks when an explicit inbox view carries an organization filter", () => {
diff --git a/ui/src/lib/task-views.ts b/ui/src/lib/task-views.ts
index 8d5a995cace..c0396b2be5d 100644
--- a/ui/src/lib/task-views.ts
+++ b/ui/src/lib/task-views.ts
@@ -76,7 +76,7 @@ const VIEWS_BY_KEY = new Map(
TASK_VIEW_GROUPS.flatMap((group) => group.views.map((view) => [view.key, view] as const)),
);
-export const DEFAULT_TASK_VIEW: TaskViewKey = "mine";
+export const DEFAULT_TASK_VIEW: TaskViewKey = "all";
export const TASK_VIEW_PARAM = "view";
export const TASK_LAST_VIEW_KEY = "paperclip:tasks:last-view";
diff --git a/ui/src/pages/CoordinatorTaskViews.tsx b/ui/src/pages/CoordinatorTaskViews.tsx
index 227fda7f520..ca1d9cce125 100644
--- a/ui/src/pages/CoordinatorTaskViews.tsx
+++ b/ui/src/pages/CoordinatorTaskViews.tsx
@@ -6,7 +6,7 @@ import { useCompany } from "@/context/CompanyContext";
import { useDialogActions } from "@/context/DialogContext";
import { useCoordinatorWorkspaceEnabled } from "@/hooks/useCoordinatorWorkspaceEnabled";
import { loadTaskListView, taskListViewStorageKey } from "@/lib/task-list-views";
-import { ORGANIZATION_SCOPED_PARAMS, normalizeTaskViewKey, resolveInitialTaskView, taskView, type TaskViewKey } from "@/lib/task-views";
+import { DEFAULT_TASK_VIEW, ORGANIZATION_SCOPED_PARAMS, normalizeTaskViewKey, resolveInitialTaskView, taskView, type TaskViewKey } from "@/lib/task-views";
import { Navigate, useLocation, useNavigate, useSearchParams } from "@/lib/router";
import { CoordinatorTasks } from "./CoordinatorTasks";
import { Inbox } from "./Inbox";
@@ -15,10 +15,10 @@ function loadView(key: string, userId: string | null): TaskViewKey {
try {
const saved = normalizeTaskViewKey(localStorage.getItem(key));
if (saved) return saved;
- const previous = userId ? loadTaskListView(taskListViewStorageKey("coordinator", userId), undefined, "inbox") : "inbox";
+ const previous = userId ? loadTaskListView(taskListViewStorageKey("coordinator", userId)) : DEFAULT_TASK_VIEW;
return previous === "inbox" ? "mine" : previous === "blocked" ? "needs_attention" : previous;
} catch {
- return "mine";
+ return DEFAULT_TASK_VIEW;
}
}
diff --git a/ui/src/pages/CoordinatorTasks.test.tsx b/ui/src/pages/CoordinatorTasks.test.tsx
index 3b038bc6692..1ad5885cde5 100644
--- a/ui/src/pages/CoordinatorTasks.test.tsx
+++ b/ui/src/pages/CoordinatorTasks.test.tsx
@@ -69,16 +69,20 @@ describe("Coordinator tasks using the proposed table", () => {
flushSync(() => root.render());
await vi.waitFor(() => expect(host.textContent).not.toContain("Loading tasks"));
}
- it("starts new users in Inbox and keeps contextual parents of their created or assigned tasks", async () => {
+ it.each([
+ { url: "/issues", view: "all", ids: ["parent", "mine", "assigned", "unrelated"] },
+ { url: "/issues?view=inbox", view: "inbox", ids: ["parent", "mine", "assigned"] },
+ ])("opens $url in $view, retaining parent context for Inbox", async ({ url, view, ids }) => {
mocks.list.mockResolvedValue([
createIssue({ id: "parent", companyId: "company", parentId: null, createdByUserId: "other" }),
createIssue({ id: "mine", companyId: "company", parentId: "parent", createdByUserId: "person" }),
createIssue({ id: "assigned", companyId: "company", parentId: null, assigneeUserId: "person" }),
createIssue({ id: "unrelated", companyId: "company", parentId: null, createdByUserId: "other" }),
]);
- await mount("/issues");
- expect(host.querySelector("[data-selected-view]")?.textContent).toBe("inbox");
- expect([...host.querySelectorAll("[data-task-id]")].map(row => row.getAttribute("data-task-id"))).toEqual(["parent", "mine", "assigned"]);
+ await mount(url);
+ expect(host.querySelector("[data-selected-view]")?.textContent).toBe(view);
+ expect([...host.querySelectorAll("[data-task-id]")].map(row => row.getAttribute("data-task-id"))).toEqual(ids);
+ expect(host.querySelector("output")?.textContent).toBe(`?view=${view}`);
});
it.each(taskListViews)("remembers $label when leaving Tasks and returning without a view URL", async ({ value, label }) => {
await mount("/issues");
@@ -94,7 +98,7 @@ describe("Coordinator tasks using the proposed table", () => {
mocks.list.mockResolvedValue([createIssue({ id: "local", companyId: "company", parentId: null, createdByUserId: "local-board" })]);
await mount("/issues");
expect(host.querySelector('[data-task-id="local"]')).not.toBeNull();
- expect(localStorage.getItem(taskListViewStorageKey("coordinator", "local-board"))).toBe("inbox");
+ expect(localStorage.getItem(taskListViewStorageKey("coordinator", "local-board"))).toBe("all");
});
it.each(["inbox", "unread"])("keeps %s selected when applying a status filter", async view => {
const mine = createIssue({ id: "mine-running", companyId: "company", parentId: null, createdByUserId: "person", status: "in_progress" });
@@ -131,16 +135,16 @@ describe("Coordinator tasks using the proposed table", () => {
});
it("waits for the viewer identity before resolving their saved view", async () => {
const key = taskListViewStorageKey("coordinator", "person");
- localStorage.setItem(key, "all");
+ localStorage.setItem(key, "inbox");
mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: null, settled: false };
await mount("/issues");
- expect(localStorage.getItem(key)).toBe("all");
+ expect(localStorage.getItem(key)).toBe("inbox");
mocks.realFilters = false; mocks.labels = [];
mocks.identity = { userId: "person", settled: true };
await mount("/issues");
- expect(host.querySelector("[data-selected-view]")?.textContent).toBe("all");
- expect(localStorage.getItem(key)).toBe("all");
+ expect(host.querySelector("[data-selected-view]")?.textContent).toBe("inbox");
+ expect(localStorage.getItem(key)).toBe("inbox");
});
it("loads the task index without descriptions or historical diagnostics", async () => {
await mount("/issues?view=all");
diff --git a/ui/src/pages/CoordinatorTasks.tsx b/ui/src/pages/CoordinatorTasks.tsx
index 86b0273fcef..2e38c33b197 100644
--- a/ui/src/pages/CoordinatorTasks.tsx
+++ b/ui/src/pages/CoordinatorTasks.tsx
@@ -54,8 +54,8 @@ export function CoordinatorTasks({ viewControl }: { viewControl?: ReactNode } =
const urlLabels = JSON.stringify(params.getAll("label").filter(Boolean));
const urlView = params.get("view");
const viewStorageKey = taskListViewStorageKey("coordinator", userId ?? "local-board");
- const [preference, setPreference] = useState(() => ({ key: viewStorageKey, view: loadTaskListView(viewStorageKey, undefined, "inbox") }));
- const savedView = preference.key === viewStorageKey ? preference.view : loadTaskListView(viewStorageKey, undefined, "inbox");
+ const [preference, setPreference] = useState(() => ({ key: viewStorageKey, view: loadTaskListView(viewStorageKey) }));
+ const savedView = preference.key === viewStorageKey ? preference.view : loadTaskListView(viewStorageKey);
const explicitView = urlView === "needs_attention" ? "blocked" : taskListViews.find(option => option.value === urlView)?.value;
const organizationScoped = [...ORGANIZATION_SCOPED_PARAMS, "workspaceId", "status", "label", "sortField", "sortDir"].some(param => params.getAll(param).some(Boolean));
const view: TaskListView = explicitView ?? (organizationScoped || normalizeTaskViewKey(urlView) ? "all" : savedView);
diff --git a/ui/src/pages/Issues.coordinator-routing.test.tsx b/ui/src/pages/Issues.coordinator-routing.test.tsx
index 7d926b3d0f2..e2d2508b807 100644
--- a/ui/src/pages/Issues.coordinator-routing.test.tsx
+++ b/ui/src/pages/Issues.coordinator-routing.test.tsx
@@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, expect, it, vi } from "vitest";
import { Issues } from "./Issues";
import { Inbox } from "./Inbox";
+import { taskListViewStorageKey } from "../lib/task-list-views";
const mountedRoutes = vi.hoisted(() => ({ organization: [] as string[] }));
@@ -69,23 +70,31 @@ it.each(["mine", "blocked", "recent", "everything"])("keeps shared %s label link
} finally { cleanup(); }
});
-it("defaults Tasks to Mine and remembers a deliberate organization view", async () => {
+it("defaults Tasks to All tasks and remembers a deliberate personal view", async () => {
const first = render("/ORG/issues");
try {
- await vi.waitFor(() => expect(first.host.querySelector("output")?.textContent).toBe("/ORG/issues?view=mine"));
- expect(first.host.textContent).toContain("Tasks Inbox owner:mine");
+ await vi.waitFor(() => expect(first.host.querySelector("output")?.textContent).toBe("/ORG/issues?view=all"));
+ expect(first.host.textContent).toContain("Coordinator tasks");
} finally { first.cleanup(); }
- const selected = render("/ORG/issues?view=done");
+ const selected = render("/ORG/issues?view=mine");
try {
- await vi.waitFor(() => expect(localStorage.getItem(JSON.stringify(["paperclip:coordinator-task-view:v2", "company", "viewer"]))).toBe("done"));
+ await vi.waitFor(() => expect(localStorage.getItem(JSON.stringify(["paperclip:coordinator-task-view:v2", "company", "viewer"]))).toBe("mine"));
} finally { selected.cleanup(); }
const returned = render("/ORG/issues");
try {
- await vi.waitFor(() => expect(returned.host.querySelector("output")?.textContent).toBe("/ORG/issues?view=done"));
- expect(returned.host.textContent).toContain("Coordinator tasks");
+ await vi.waitFor(() => expect(returned.host.querySelector("output")?.textContent).toBe("/ORG/issues?view=mine"));
+ expect(returned.host.textContent).toContain("Tasks Inbox owner:mine");
} finally { returned.cleanup(); }
});
+it.each([["inbox", "mine"], ["blocked", "needs_attention"], ["all", "all"]])("preserves a saved %s view from the previous task picker", async (previous, view) => {
+ localStorage.setItem(taskListViewStorageKey("coordinator", "viewer"), previous);
+ const { host, cleanup } = render("/ORG/issues");
+ try {
+ await vi.waitFor(() => expect(host.querySelector("output")?.textContent).toBe(`/ORG/issues?view=${view}`));
+ } finally { cleanup(); }
+});
+
it.each(["mine", "blocked", "recent", "everything"])("normalizes a filtered %s link before mounting its task collection", async view => {
const { host, cleanup } = render(`/ORG/issues?view=${view}&label=qa&keep=value#saved`);
try {