diff --git a/apps/web/app/(dashboard)/issues/page.test.tsx b/apps/web/app/(dashboard)/issues/page.test.tsx
index 8b2a143e..c48dc4f8 100644
--- a/apps/web/app/(dashboard)/issues/page.test.tsx
+++ b/apps/web/app/(dashboard)/issues/page.test.tsx
@@ -339,23 +339,26 @@ describe("IssuesPage", () => {
expect(screen.getByText("Issues")).toBeInTheDocument();
});
- it("shows 'New Issue' button", () => {
+ it("shows scope buttons", () => {
mockStoreState.loading = false;
mockStoreState.issues = [];
render();
- expect(screen.getByText("New Issue")).toBeInTheDocument();
+ expect(screen.getByText("All")).toBeInTheDocument();
+ expect(screen.getByText("Members")).toBeInTheDocument();
+ expect(screen.getByText("Agents")).toBeInTheDocument();
});
- it("shows filter buttons", () => {
+ it("shows filter and display icon buttons", () => {
mockStoreState.loading = false;
mockStoreState.issues = mockIssues;
render();
- expect(screen.getByText("Filter")).toBeInTheDocument();
- expect(screen.getByText("Display")).toBeInTheDocument();
+ // Filter and Display are now icon-only buttons, verify they render as buttons
+ const buttons = screen.getAllByRole("button");
+ expect(buttons.length).toBeGreaterThan(0);
});
it("shows empty board view when no issues exist", () => {
diff --git a/apps/web/features/issues/components/issues-header.tsx b/apps/web/features/issues/components/issues-header.tsx
index 5b59b63c..8574013d 100644
--- a/apps/web/features/issues/components/issues-header.tsx
+++ b/apps/web/features/issues/components/issues-header.tsx
@@ -10,7 +10,6 @@ import {
Columns3,
Filter,
List,
- Plus,
SignalHigh,
SlidersHorizontal,
User,
@@ -18,7 +17,6 @@ import {
UserPen,
} from "lucide-react";
import { Button } from "@/components/ui/button";
-import { useIssueStore } from "@/features/issues/store";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -38,7 +36,6 @@ import {
PopoverContent,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
-import { useModalStore } from "@/features/modals";
import {
ALL_STATUSES,
STATUS_CONFIG,
@@ -54,13 +51,16 @@ import {
CARD_PROPERTY_OPTIONS,
type ActorFilterValue,
} from "@/features/issues/stores/view-store";
+import {
+ useIssuesScopeStore,
+ type IssuesScope,
+} from "@/features/issues/stores/issues-scope-store";
import { filterIssues } from "@/features/issues/utils/filter";
+import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import type { Issue } from "@/shared/types";
// ---------------------------------------------------------------------------
// HoverCheck — shadcn official pattern (PR #6862)
-// Uses data-selected attr instead of Checkbox component to avoid
-// DropdownMenuCheckboxItem's focus:**:text-accent-foreground cascade.
// ---------------------------------------------------------------------------
const FILTER_ITEM_CLASS =
@@ -123,6 +123,16 @@ function useIssueCounts(allIssues: Issue[]) {
}, [allIssues]);
}
+// ---------------------------------------------------------------------------
+// Scope config
+// ---------------------------------------------------------------------------
+
+const SCOPES: { value: IssuesScope; label: string; description: string }[] = [
+ { value: "all", label: "All", description: "All issues in this workspace" },
+ { value: "members", label: "Members", description: "Issues assigned to team members" },
+ { value: "agents", label: "Agents", description: "Issues assigned to AI agents" },
+];
+
// ---------------------------------------------------------------------------
// Actor sub-menu content (shared between Assignee and Creator)
// ---------------------------------------------------------------------------
@@ -262,7 +272,10 @@ function ActorSubContent({
// IssuesHeader
// ---------------------------------------------------------------------------
-export function IssuesHeader() {
+export function IssuesHeader({ scopedIssues }: { scopedIssues: Issue[] }) {
+ const scope = useIssuesScopeStore((s) => s.scope);
+ const setScope = useIssuesScopeStore((s) => s.setScope);
+
const viewMode = useIssueViewStore((s) => s.viewMode);
const statusFilters = useIssueViewStore((s) => s.statusFilters);
const priorityFilters = useIssueViewStore((s) => s.priorityFilters);
@@ -274,74 +287,69 @@ export function IssuesHeader() {
const cardProperties = useIssueViewStore((s) => s.cardProperties);
const act = useIssueViewStore.getState();
- const allIssues = useIssueStore((s) => s.issues);
- const counts = useIssueCounts(allIssues);
+ const counts = useIssueCounts(scopedIssues);
- const filteredCount = useMemo(
- () => filterIssues(allIssues, { statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters }).length,
- [allIssues, statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters],
- );
-
- const filterCount = getActiveFilterCount({
- statusFilters,
- priorityFilters,
- assigneeFilters,
- includeNoAssignee,
- creatorFilters,
- });
+ const hasActiveFilters =
+ getActiveFilterCount({
+ statusFilters,
+ priorityFilters,
+ assigneeFilters,
+ includeNoAssignee,
+ creatorFilters,
+ }) > 0;
const sortLabel =
SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? "Manual";
- const hasActiveFilters = filterCount > 0;
return (
-
- {/* View toggle */}
-
-
- {viewMode === "board" ? :
}
- {viewMode === "board" ? "Board" : "List"}
-
- }
- />
-
-
- View
- act.setViewMode("board")}>
-
- Board
-
- act.setViewMode("list")}>
-
- List
-
-
-
-
+ {/* Left: scope buttons */}
+
+ {SCOPES.map((s) => (
+
+ setScope(s.value)}
+ >
+ {s.label}
+
+ }
+ />
+ {s.description}
+
+ ))}
+
- {/* Filter — DropdownMenu with sub-menus */}
+ {/* Right: filter + display + view toggle */}
+
+ {/* Filter */}
-
-
- Filter
- {hasActiveFilters && (
-
- {filterCount}
-
- )}
-
- }
- />
-
+
+
+
+ {hasActiveFilters && (
+
+ )}
+
+ }
+ />
+ }
+ />
+ Filter
+
+
{/* Status */}
@@ -472,15 +480,21 @@ export function IssuesHeader() {
{/* Display settings */}
-
-
- Display
-
- }
- />
-
+
+
+
+
+ }
+ />
+ }
+ />
+ Display settings
+
+
Ordering
@@ -549,19 +563,43 @@ export function IssuesHeader() {
-
-
-
- {filteredCount} {filteredCount === 1 ? "Issue" : "Issues"}
-
-
+ {/* View toggle */}
+
+
+
+ {viewMode === "board" ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+ }
+ />
+
+ {viewMode === "board" ? "Board view" : "List view"}
+
+
+
+
+ View
+ act.setViewMode("board")}>
+
+ Board
+
+ act.setViewMode("list")}>
+
+ List
+
+
+
+
);
diff --git a/apps/web/features/issues/components/issues-page.tsx b/apps/web/features/issues/components/issues-page.tsx
index f09b4e7f..84ad32e6 100644
--- a/apps/web/features/issues/components/issues-page.tsx
+++ b/apps/web/features/issues/components/issues-page.tsx
@@ -7,6 +7,7 @@ import type { IssueStatus } from "@/shared/types";
import { Skeleton } from "@/components/ui/skeleton";
import { useIssueStore } from "@/features/issues/store";
import { useIssueViewStore, initFilterWorkspaceSync } from "@/features/issues/stores/view-store";
+import { useIssuesScopeStore } from "@/features/issues/stores/issues-scope-store";
import { ViewStoreProvider } from "@/features/issues/stores/view-store-context";
import { filterIssues } from "@/features/issues/utils/filter";
import { BOARD_STATUSES } from "@/features/issues/config";
@@ -23,6 +24,7 @@ export function IssuesPage() {
const allIssues = useIssueStore((s) => s.issues);
const loading = useIssueStore((s) => s.loading);
const workspace = useWorkspaceStore((s) => s.workspace);
+ const scope = useIssuesScopeStore((s) => s.scope);
const viewMode = useIssueViewStore((s) => s.viewMode);
const statusFilters = useIssueViewStore((s) => s.statusFilters);
const priorityFilters = useIssueViewStore((s) => s.priorityFilters);
@@ -36,11 +38,20 @@ export function IssuesPage() {
useEffect(() => {
useIssueSelectionStore.getState().clear();
- }, [viewMode]);
+ }, [viewMode, scope]);
+
+ // Scope pre-filter: narrow by assignee type
+ const scopedIssues = useMemo(() => {
+ if (scope === "members")
+ return allIssues.filter((i) => i.assignee_type === "member");
+ if (scope === "agents")
+ return allIssues.filter((i) => i.assignee_type === "agent");
+ return allIssues;
+ }, [allIssues, scope]);
const issues = useMemo(
- () => filterIssues(allIssues, { statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters }),
- [allIssues, statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters],
+ () => filterIssues(scopedIssues, { statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters }),
+ [scopedIssues, statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters],
);
const visibleStatuses = useMemo(() => {
@@ -115,8 +126,8 @@ export function IssuesPage() {
Issues
- {/* Header 2: View toggle + filters */}
-
+ {/* Header 2: Scope tabs + filters */}
+
{/* Content: scrollable */}
@@ -124,7 +135,7 @@ export function IssuesPage() {
{viewMode === "board" ? (
void;
+}
+
+export const useIssuesScopeStore = create()(
+ persist(
+ (set) => ({
+ scope: "all",
+ setScope: (scope) => set({ scope }),
+ }),
+ { name: "multica_issues_scope" },
+ ),
+);
diff --git a/apps/web/features/issues/stores/view-store.ts b/apps/web/features/issues/stores/view-store.ts
index 01dae9f1..a77759b3 100644
--- a/apps/web/features/issues/stores/view-store.ts
+++ b/apps/web/features/issues/stores/view-store.ts
@@ -63,7 +63,7 @@ export interface IssueViewState {
toggleListCollapsed: (status: IssueStatus) => void;
}
-const viewStoreSlice = (set: StoreApi["setState"]): IssueViewState => ({
+export const viewStoreSlice = (set: StoreApi["setState"]): IssueViewState => ({
viewMode: "board",
statusFilters: [],
priorityFilters: [],
@@ -162,7 +162,7 @@ const viewStoreSlice = (set: StoreApi["setState"]): IssueViewSta
})),
});
-const viewStorePersistOptions = (name: string) => ({
+export const viewStorePersistOptions = (name: string) => ({
name,
partialize: (state: IssueViewState) => ({
viewMode: state.viewMode,
diff --git a/apps/web/features/my-issues/components/my-issues-header.tsx b/apps/web/features/my-issues/components/my-issues-header.tsx
index eb934987..f1f24aee 100644
--- a/apps/web/features/my-issues/components/my-issues-header.tsx
+++ b/apps/web/features/my-issues/components/my-issues-header.tsx
@@ -11,7 +11,6 @@ import {
Columns3,
Filter,
List,
- Plus,
SignalHigh,
SlidersHorizontal,
} from "lucide-react";
@@ -35,7 +34,6 @@ import {
PopoverContent,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
-import { useModalStore } from "@/features/modals";
import {
ALL_STATUSES,
STATUS_CONFIG,
@@ -48,11 +46,12 @@ import {
CARD_PROPERTY_OPTIONS,
} from "@/features/issues/stores/view-store";
import { filterIssues } from "@/features/issues/utils/filter";
+import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import type { Issue } from "@/shared/types";
-import { myIssuesViewStore } from "../stores/my-issues-view-store";
+import { myIssuesViewStore, type MyIssuesScope } from "../stores/my-issues-view-store";
// ---------------------------------------------------------------------------
-// HoverCheck — shadcn official pattern (PR #6862)
+// HoverCheck
// ---------------------------------------------------------------------------
const FILTER_ITEM_CLASS =
@@ -97,6 +96,16 @@ function useIssueCounts(allIssues: Issue[]) {
}, [allIssues]);
}
+// ---------------------------------------------------------------------------
+// Scope config
+// ---------------------------------------------------------------------------
+
+const SCOPES: { value: MyIssuesScope; label: string; description: string }[] = [
+ { value: "assigned", label: "Assigned", description: "Issues assigned to me" },
+ { value: "created", label: "Created", description: "Issues I created" },
+ { value: "agents", label: "My Agents", description: "Issues assigned to my agents" },
+];
+
// ---------------------------------------------------------------------------
// MyIssuesHeader
// ---------------------------------------------------------------------------
@@ -108,82 +117,66 @@ export function MyIssuesHeader({ allIssues }: { allIssues: Issue[] }) {
const sortBy = useStore(myIssuesViewStore, (s) => s.sortBy);
const sortDirection = useStore(myIssuesViewStore, (s) => s.sortDirection);
const cardProperties = useStore(myIssuesViewStore, (s) => s.cardProperties);
+ const scope = useStore(myIssuesViewStore, (s) => s.scope);
const act = myIssuesViewStore.getState();
const counts = useIssueCounts(allIssues);
- const filteredCount = useMemo(
- () =>
- filterIssues(allIssues, {
- statusFilters,
- priorityFilters,
- assigneeFilters: [],
- includeNoAssignee: false,
- creatorFilters: [],
- }).length,
- [allIssues, statusFilters, priorityFilters],
- );
-
- const filterCount = getActiveFilterCount({ statusFilters, priorityFilters });
+ const hasActiveFilters =
+ getActiveFilterCount({ statusFilters, priorityFilters }) > 0;
const sortLabel =
SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? "Manual";
- const hasActiveFilters = filterCount > 0;
return (
-
- {/* View toggle */}
-
-
- {viewMode === "board" ? (
-
- ) : (
-
- )}
- {viewMode === "board" ? "Board" : "List"}
-
- }
- />
-
-
- View
- act.setViewMode("board")}>
-
- Board
-
- act.setViewMode("list")}>
-
- List
-
-
-
-
+ {/* Left: scope buttons */}
+
+ {SCOPES.map((s) => (
+
+ act.setScope(s.value)}
+ >
+ {s.label}
+
+ }
+ />
+ {s.description}
+
+ ))}
+
- {/* Filter — DropdownMenu with sub-menus */}
+ {/* Right: filter + display + view toggle */}
+
+ {/* Filter */}
-
-
- Filter
- {hasActiveFilters && (
-
- {filterCount}
-
- )}
-
- }
- />
-
+
+
+
+ {hasActiveFilters && (
+
+ )}
+
+ }
+ />
+ }
+ />
+ Filter
+
+
{/* Status */}
@@ -270,15 +263,21 @@ export function MyIssuesHeader({ allIssues }: { allIssues: Issue[] }) {
{/* Display settings */}
-
-
- Display
-
- }
- />
-
+
+
+
+
+ }
+ />
+ }
+ />
+ Display settings
+
+
Ordering
@@ -349,19 +348,43 @@ export function MyIssuesHeader({ allIssues }: { allIssues: Issue[] }) {
-
-
-
- {filteredCount} {filteredCount === 1 ? "Issue" : "Issues"}
-
-
+ {/* View toggle */}
+
+
+
+ {viewMode === "board" ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+ }
+ />
+
+ {viewMode === "board" ? "Board view" : "List view"}
+
+
+
+
+ View
+ act.setViewMode("board")}>
+
+ Board
+
+ act.setViewMode("list")}>
+
+ List
+
+
+
+
);
diff --git a/apps/web/features/my-issues/components/my-issues-page.tsx b/apps/web/features/my-issues/components/my-issues-page.tsx
index 757c4b49..d8bcf855 100644
--- a/apps/web/features/my-issues/components/my-issues-page.tsx
+++ b/apps/web/features/my-issues/components/my-issues-page.tsx
@@ -31,6 +31,7 @@ export function MyIssuesPage() {
const viewMode = useStore(myIssuesViewStore, (s) => s.viewMode);
const statusFilters = useStore(myIssuesViewStore, (s) => s.statusFilters);
const priorityFilters = useStore(myIssuesViewStore, (s) => s.priorityFilters);
+ const scope = useStore(myIssuesViewStore, (s) => s.scope);
useEffect(() => {
registerViewStoreForWorkspaceSync(myIssuesViewStore);
@@ -38,7 +39,7 @@ export function MyIssuesPage() {
useEffect(() => {
useIssueSelectionStore.getState().clear();
- }, [viewMode]);
+ }, [viewMode, scope]);
const myAgentIds = useMemo(() => {
if (!user) return new Set
();
@@ -47,19 +48,40 @@ export function MyIssuesPage() {
);
}, [agents, user]);
- // Pre-filter: union of (assigned to me + my agents + created by me)
- const myIssues = useMemo(() => {
+ // Per-scope issue lists
+ const assignedToMe = useMemo(() => {
+ if (!user) return [];
+ return allIssues.filter(
+ (i) => i.assignee_type === "member" && i.assignee_id === user.id,
+ );
+ }, [allIssues, user]);
+
+ const myAgentIssues = useMemo(() => {
if (!user) return [];
return allIssues.filter(
(i) =>
- (i.assignee_type === "member" && i.assignee_id === user.id) ||
- (i.assignee_type === "agent" &&
- i.assignee_id &&
- myAgentIds.has(i.assignee_id)) ||
- (i.creator_type === "member" && i.creator_id === user.id),
+ i.assignee_type === "agent" &&
+ i.assignee_id &&
+ myAgentIds.has(i.assignee_id),
);
}, [allIssues, user, myAgentIds]);
+ const createdByMe = useMemo(() => {
+ if (!user) return [];
+ return allIssues.filter(
+ (i) => i.creator_type === "member" && i.creator_id === user.id,
+ );
+ }, [allIssues, user]);
+
+ const myIssues = useMemo(() => {
+ switch (scope) {
+ case "assigned": return assignedToMe;
+ case "agents": return myAgentIssues;
+ case "created": return createdByMe;
+ default: return assignedToMe;
+ }
+ }, [scope, assignedToMe, myAgentIssues, createdByMe]);
+
// Apply status/priority filters from view store
const issues = useMemo(
() =>
@@ -144,7 +166,7 @@ export function MyIssuesPage() {
My Issues
- {/* Header 2: View toggle + filters */}
+ {/* Header: scope tabs (left) + controls (right) */}
{/* Content: scrollable */}
diff --git a/apps/web/features/my-issues/stores/my-issues-view-store.ts b/apps/web/features/my-issues/stores/my-issues-view-store.ts
index 3f59c776..0b97406f 100644
--- a/apps/web/features/my-issues/stores/my-issues-view-store.ts
+++ b/apps/web/features/my-issues/stores/my-issues-view-store.ts
@@ -1,5 +1,35 @@
"use client";
-import { createIssueViewStore } from "@/features/issues/stores/view-store";
+import { createStore, type StoreApi } from "zustand/vanilla";
+import { persist } from "zustand/middleware";
+import {
+ type IssueViewState,
+ viewStoreSlice,
+ viewStorePersistOptions,
+} from "@/features/issues/stores/view-store";
-export const myIssuesViewStore = createIssueViewStore("multica_my_issues_view");
+export type MyIssuesScope = "assigned" | "created" | "agents";
+
+export interface MyIssuesViewState extends IssueViewState {
+ scope: MyIssuesScope;
+ setScope: (scope: MyIssuesScope) => void;
+}
+
+const basePersist = viewStorePersistOptions("multica_my_issues_view");
+
+export const myIssuesViewStore: StoreApi = createStore()(
+ persist(
+ (set) => ({
+ ...viewStoreSlice(set as unknown as StoreApi["setState"]),
+ scope: "assigned" as MyIssuesScope,
+ setScope: (scope: MyIssuesScope) => set({ scope }),
+ }),
+ {
+ name: basePersist.name,
+ partialize: (state: MyIssuesViewState) => ({
+ ...basePersist.partialize(state),
+ scope: state.scope,
+ }),
+ },
+ ),
+);