diff --git a/ui/src/pages/Inbox.test.tsx b/ui/src/pages/Inbox.test.tsx
index 4a3c61994e6..cc1300d489d 100644
--- a/ui/src/pages/Inbox.test.tsx
+++ b/ui/src/pages/Inbox.test.tsx
@@ -5,7 +5,7 @@ import type { ComponentProps } from "react";
import { createRoot } from "react-dom/client";
import type { Issue } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { FailedRunInboxRow, InboxIssueMetaLeading, InboxIssueTrailingColumns } from "./Inbox";
+import { FailedRunInboxRow, InboxGroupHeader, InboxIssueMetaLeading, InboxIssueTrailingColumns } from "./Inbox";
vi.mock("@/lib/router", () => ({
Link: ({ children, className, ...props }: ComponentProps<"a">) => (
@@ -245,3 +245,52 @@ describe("InboxIssueTrailingColumns", () => {
});
});
});
+
+describe("InboxGroupHeader", () => {
+ let container: HTMLDivElement;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ container.remove();
+ });
+
+ it("shows a left caret and expanded state for collapsible mobile headers", () => {
+ const root = createRoot(container);
+
+ act(() => {
+ root.render( );
+ });
+
+ const button = container.querySelector("button");
+ expect(button).not.toBeNull();
+ expect(button?.getAttribute("aria-expanded")).toBe("true");
+ expect(button?.textContent).toContain("Primary workspace (default)");
+ const caret = container.querySelector("svg");
+ expect(caret?.className.baseVal).toContain("rotate-90");
+
+ act(() => {
+ root.unmount();
+ });
+ });
+
+ it("keeps the caret collapsed when the mobile group is hidden", () => {
+ const root = createRoot(container);
+
+ act(() => {
+ root.render( );
+ });
+
+ const button = container.querySelector("button");
+ expect(button?.getAttribute("aria-expanded")).toBe("false");
+ const caret = container.querySelector("svg");
+ expect(caret?.className.baseVal).not.toContain("rotate-90");
+
+ act(() => {
+ root.unmount();
+ });
+ });
+});
diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx
index c438bea1f05..96d53d30312 100644
--- a/ui/src/pages/Inbox.tsx
+++ b/ui/src/pages/Inbox.tsx
@@ -149,6 +149,47 @@ type InboxGroupedSection = {
isArchivedSearch: boolean;
};
+export function InboxGroupHeader({
+ label,
+ collapsible = false,
+ collapsed = false,
+ onToggle,
+}: {
+ label: string;
+ collapsible?: boolean;
+ collapsed?: boolean;
+ onToggle?: () => void;
+}) {
+ if (collapsible) {
+ return (
+
+
+
+ {label}
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
function buildGroupedInboxSections(
items: InboxWorkItem[],
groupBy: InboxWorkItemGroupBy,
@@ -1101,6 +1142,15 @@ export function Inbox() {
});
}, []);
const [collapsedInboxParents, setCollapsedInboxParents] = useState>(new Set());
+ const [collapsedGroupKeys, setCollapsedGroupKeys] = useState>(new Set());
+ const toggleGroupCollapse = useCallback((groupKey: string) => {
+ setCollapsedGroupKeys((prev) => {
+ const next = new Set(prev);
+ if (next.has(groupKey)) next.delete(groupKey);
+ else next.add(groupKey);
+ return next;
+ });
+ }, []);
const groupedSections = useMemo(() => [
...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled, inboxWorkspaceGrouping),
...buildGroupedInboxSections(
@@ -2063,6 +2113,8 @@ export function Inbox() {
let previousTimestamp = Number.POSITIVE_INFINITY;
return groupedSections.flatMap((group, groupIndex) => {
const elements: ReactNode[] = [];
+ const mobileWorkspaceGrouping = isMobile && groupBy === "workspace";
+ const isGroupCollapsed = mobileWorkspaceGrouping && collapsedGroupKeys.has(group.key);
if (group.isArchivedSearch && (groupIndex === 0 || !groupedSections[groupIndex - 1]?.isArchivedSearch)) {
elements.push(
0 && "border-t border-border",
)}
>
-
-
-
- {group.label}
-
-
-
+
toggleGroupCollapse(group.key) : undefined}
+ />
,
);
}
+ if (isGroupCollapsed) return elements;
for (let index = 0; index < group.displayItems.length; index += 1) {
const item = group.displayItems[index]!;