vi.fn());
+
+vi.mock("@/api/issues", () => ({
+ issuesApi: {
+ get: mockIssuesApiGet,
+ },
+}));
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+function createIssue(overrides: Partial
= {}): Issue {
+ return {
+ id: "issue-1",
+ identifier: "PAP-1",
+ companyId: "company-1",
+ projectId: null,
+ projectWorkspaceId: null,
+ goalId: null,
+ parentId: null,
+ title: "Quicklook title",
+ description: "Quicklook description",
+ status: "todo",
+ priority: "medium",
+ assigneeAgentId: null,
+ assigneeUserId: null,
+ createdByAgentId: null,
+ createdByUserId: null,
+ issueNumber: 1,
+ requestDepth: 0,
+ billingCode: null,
+ assigneeAdapterOverrides: null,
+ executionWorkspaceId: null,
+ executionWorkspacePreference: null,
+ executionWorkspaceSettings: null,
+ checkoutRunId: null,
+ executionRunId: null,
+ executionAgentNameKey: null,
+ executionLockedAt: null,
+ startedAt: null,
+ completedAt: null,
+ cancelledAt: null,
+ hiddenAt: null,
+ createdAt: new Date("2026-05-01T00:00:00.000Z"),
+ updatedAt: new Date("2026-05-01T00:00:00.000Z"),
+ labels: [],
+ labelIds: [],
+ myLastTouchAt: null,
+ lastExternalCommentAt: null,
+ isUnreadForMe: false,
+ workMode: "standard",
+ ...overrides,
+ };
+}
+
+describe("IssueLinkQuicklook", () => {
+ let container: HTMLDivElement;
+ let root: Root;
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+ mockIssuesApiGet.mockResolvedValue(createIssue());
+ });
+
+ afterEach(() => {
+ act(() => {
+ root.unmount();
+ });
+ queryClient.clear();
+ container.remove();
+ vi.useRealTimers();
+ vi.clearAllMocks();
+ });
+
+ it("keeps portaled quicklook links mounted until after blur click handling", () => {
+ const issue = createIssue();
+
+ act(() => {
+ root.render(
+
+
+
+ PAP-1
+
+
+ ,
+ );
+ });
+
+ const trigger = container.querySelector("a") as HTMLAnchorElement | null;
+ expect(trigger).not.toBeNull();
+
+ act(() => {
+ trigger?.focus();
+ });
+
+ expect(document.body.textContent).toContain("Quicklook title");
+
+ act(() => {
+ trigger?.blur();
+ });
+
+ expect(document.body.textContent).toContain("Quicklook title");
+
+ act(() => {
+ vi.runOnlyPendingTimers();
+ });
+
+ expect(document.body.textContent).not.toContain("Quicklook title");
+ });
+});
diff --git a/ui/src/components/IssueLinkQuicklook.tsx b/ui/src/components/IssueLinkQuicklook.tsx
index 16b16822..65a07b1f 100644
--- a/ui/src/components/IssueLinkQuicklook.tsx
+++ b/ui/src/components/IssueLinkQuicklook.tsx
@@ -75,6 +75,8 @@ export const IssueLinkQuicklook = React.forwardRef<
issuePathId: string;
disableIssueQuicklook?: boolean;
issuePrefetch?: Issue | null;
+ issueQuicklookSide?: React.ComponentProps["side"];
+ issueQuicklookAlign?: React.ComponentProps["align"];
}
>(function IssueLinkQuicklookImpl(
{
@@ -85,10 +87,13 @@ export const IssueLinkQuicklook = React.forwardRef<
state,
disableIssueQuicklook = false,
issuePrefetch = null,
+ issueQuicklookSide = "top",
+ issueQuicklookAlign = "start",
onClick,
onClickCapture,
onMouseEnter,
onFocus,
+ onBlur,
onTouchStart,
...props
},
@@ -119,8 +124,14 @@ export const IssueLinkQuicklook = React.forwardRef<
}}
onFocus={(event) => {
handlePrefetch();
+ setOpen(true);
onFocus?.(event);
}}
+ onBlur={(event) => {
+ // Let clicks inside the portaled quicklook content finish before closing.
+ setTimeout(() => setOpen(false), 0);
+ onBlur?.(event);
+ }}
onTouchStart={(event) => {
handlePrefetch();
onTouchStart?.(event);
@@ -157,8 +168,8 @@ export const IssueLinkQuicklook = React.forwardRef<
setOpen(true)}
onMouseLeave={() => setOpen(false)}
onOpenAutoFocus={(event) => event.preventDefault()}
diff --git a/ui/src/components/IssueSiblingNavigation.test.tsx b/ui/src/components/IssueSiblingNavigation.test.tsx
new file mode 100644
index 00000000..d356fcfb
--- /dev/null
+++ b/ui/src/components/IssueSiblingNavigation.test.tsx
@@ -0,0 +1,130 @@
+// @vitest-environment jsdom
+
+import { act, type AnchorHTMLAttributes, type ReactNode } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import type { Issue } from "@paperclipai/shared";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { IssueSiblingNavigation } from "./IssueSiblingNavigation";
+
+vi.mock("@/lib/router", () => ({
+ Link: ({
+ children,
+ to,
+ issueQuicklookAlign,
+ issueQuicklookSide,
+ issuePrefetch: _issuePrefetch,
+ state: _state,
+ ...props
+ }: AnchorHTMLAttributes & {
+ to: string;
+ issueQuicklookAlign?: string;
+ issueQuicklookSide?: string;
+ issuePrefetch?: unknown;
+ state?: unknown;
+ children?: ReactNode;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+function issue(id: string, overrides: Partial = {}): Issue {
+ return {
+ id,
+ identifier: `PAP-${id}`,
+ title: `Sibling ${id}`,
+ status: "todo",
+ blockerAttention: null,
+ createdAt: new Date("2026-05-01T00:00:00.000Z"),
+ updatedAt: new Date("2026-05-01T00:00:00.000Z"),
+ ...overrides,
+ } as Issue;
+}
+
+let root: Root | null = null;
+let container: HTMLDivElement | null = null;
+
+afterEach(() => {
+ if (root) {
+ act(() => root?.unmount());
+ }
+ root = null;
+ container?.remove();
+ container = null;
+});
+
+function render(node: ReactNode) {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = createRoot(container);
+ act(() => root?.render(node));
+ return container;
+}
+
+describe("IssueSiblingNavigation", () => {
+ it("renders the locked card anatomy for previous and next siblings", () => {
+ const node = render(
+ ,
+ );
+
+ const nav = node.querySelector("nav");
+ expect(nav?.getAttribute("aria-label")).toBe("Sub-issue navigation");
+ expect(nav?.className).toContain("sm:grid-cols-2");
+ expect(nav?.className).not.toContain("border-t");
+
+ const links = Array.from(node.querySelectorAll("a"));
+ expect(links).toHaveLength(2);
+ expect(links[0].textContent).toContain("Previous");
+ expect(links[0].textContent).toContain("PAP-1");
+ expect(links[0].textContent).toContain("Previous sibling title");
+ expect(links[0].getAttribute("aria-label")).toBe("Previous sub-issue: PAP-1 - Previous sibling title");
+ expect(links[0].getAttribute("data-quicklook-align")).toBe("start");
+
+ expect(links[1].textContent).toContain("Next");
+ expect(links[1].textContent).toContain("PAP-3");
+ expect(links[1].textContent).toContain("Next sibling title");
+ expect(links[1].getAttribute("aria-label")).toBe("Next sub-issue: PAP-3 - Next sibling title");
+ expect(links[1].getAttribute("data-quicklook-align")).toBe("end");
+ expect(links[1].className).toContain("sm:text-right");
+
+ expect(links[0].className).toContain("rounded-lg");
+ expect(links[0].className).toContain("hover:bg-accent/50");
+ expect(links[0].className).toContain("focus-visible:ring-[3px]");
+ expect(node.querySelector(".truncate")?.textContent).toBe("Previous sibling title");
+ });
+
+ it("keeps a lone next card in the right desktop column", () => {
+ const node = render(
+ ,
+ );
+
+ const links = Array.from(node.querySelectorAll("a"));
+ expect(links).toHaveLength(1);
+ expect(links[0].textContent).toContain("Next");
+ expect(links[0].className).toContain("sm:col-start-2");
+ expect(node.textContent).not.toContain("Previous");
+ });
+});
diff --git a/ui/src/components/IssueSiblingNavigation.tsx b/ui/src/components/IssueSiblingNavigation.tsx
new file mode 100644
index 00000000..213dfe4d
--- /dev/null
+++ b/ui/src/components/IssueSiblingNavigation.tsx
@@ -0,0 +1,90 @@
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import type { Issue } from "@paperclipai/shared";
+import type { IssueSiblingNavigation as IssueSiblingNavigationState } from "@/lib/issue-detail-subissues";
+import { createIssueDetailPath, withIssueDetailHeaderSeed } from "@/lib/issueDetailBreadcrumb";
+import { cn } from "@/lib/utils";
+import { Link } from "@/lib/router";
+import { StatusIcon } from "./StatusIcon";
+
+type IssueSiblingNavigationProps = {
+ navigation: IssueSiblingNavigationState | null;
+ linkState?: unknown;
+};
+
+export function IssueSiblingNavigation({ navigation, linkState }: IssueSiblingNavigationProps) {
+ if (!navigation) return null;
+
+ return (
+
+ );
+}
+
+function SiblingLink({
+ direction,
+ issue,
+ linkState,
+ className,
+}: {
+ direction: "previous" | "next";
+ issue: Issue;
+ linkState?: unknown;
+ className?: string;
+}) {
+ const issuePathId = issue.identifier ?? issue.id;
+ const label = direction === "previous" ? "Previous" : "Next";
+ const ariaDirection = direction === "previous" ? "Previous sub-issue" : "Next sub-issue";
+ const identifier = issue.identifier ?? issue.id.slice(0, 8);
+ const Icon = direction === "previous" ? ChevronLeft : ChevronRight;
+
+ return (
+
+
+
+ {direction === "previous" ? : null}
+ {label}
+ {direction === "next" ? : null}
+
+
+
+ {identifier}
+
+
+ {issue.title}
+
+
+
+ );
+}
diff --git a/ui/src/lib/issue-detail-subissues.test.ts b/ui/src/lib/issue-detail-subissues.test.ts
index 611e677b..53b3ead5 100644
--- a/ui/src/lib/issue-detail-subissues.test.ts
+++ b/ui/src/lib/issue-detail-subissues.test.ts
@@ -3,6 +3,7 @@
import { describe, expect, it } from "vitest";
import type { Issue } from "@paperclipai/shared";
import {
+ buildIssueSiblingNavigation,
buildSubIssueProgressSummary,
shouldRenderRichSubIssuesSection,
shouldRenderSubIssueProgressSummary,
@@ -24,6 +25,21 @@ function issue(
} as Issue;
}
+function siblingIssue(
+ id: string,
+ createdAt: string,
+ blockedByIds: string[] = [],
+ overrides: Partial = {},
+): Issue {
+ return {
+ ...issue(id, "todo", createdAt, blockedByIds),
+ parentId: "parent-1",
+ title: `Sibling ${id}`,
+ hiddenAt: null,
+ ...overrides,
+ } as Issue;
+}
+
describe("shouldRenderRichSubIssuesSection", () => {
it("shows the rich sub-issues section while child issues are loading", () => {
expect(shouldRenderRichSubIssuesSection(true, 0)).toBe(true);
@@ -78,3 +94,112 @@ describe("buildSubIssueProgressSummary", () => {
expect(summary.target?.issue.id).toBe("2");
});
});
+
+describe("buildIssueSiblingNavigation", () => {
+ it("orders linear blocker chains before selecting previous and next siblings", () => {
+ const current = siblingIssue("2", "2026-04-02T00:00:00.000Z", ["1"]);
+ const navigation = buildIssueSiblingNavigation(current, [
+ siblingIssue("3", "2026-04-03T00:00:00.000Z", ["2"]),
+ siblingIssue("1", "2026-04-01T00:00:00.000Z"),
+ current,
+ ]);
+
+ expect(navigation?.previous?.id).toBe("1");
+ expect(navigation?.next?.id).toBe("3");
+ expect(navigation?.currentIndex).toBe(1);
+ expect(navigation?.totalCount).toBe(3);
+ });
+
+ it("degrades branch and merge graphs to stable workflow order", () => {
+ const current = siblingIssue("3", "2026-04-03T00:00:00.000Z", ["1"]);
+ const navigation = buildIssueSiblingNavigation(current, [
+ siblingIssue("4", "2026-04-04T00:00:00.000Z", ["2", "3"]),
+ siblingIssue("2", "2026-04-02T00:00:00.000Z", ["1"]),
+ current,
+ siblingIssue("1", "2026-04-01T00:00:00.000Z"),
+ ]);
+
+ expect(navigation?.previous?.id).toBe("2");
+ expect(navigation?.next?.id).toBe("4");
+ });
+
+ it("falls back to created time and id when siblings have no direct blocker hints", () => {
+ const current = siblingIssue("2", "2026-04-01T00:00:00.000Z");
+ const navigation = buildIssueSiblingNavigation(current, [
+ siblingIssue("3", "2026-04-02T00:00:00.000Z"),
+ siblingIssue("1", "2026-04-01T00:00:00.000Z"),
+ current,
+ ]);
+
+ expect(navigation?.previous?.id).toBe("1");
+ expect(navigation?.next?.id).toBe("3");
+ });
+
+ it("hides navigation for root issues without children or hidden current issues", () => {
+ expect(buildIssueSiblingNavigation(siblingIssue("1", "2026-04-01T00:00:00.000Z", [], { parentId: null }), []))
+ .toBeNull();
+ expect(buildIssueSiblingNavigation(siblingIssue("1", "2026-04-01T00:00:00.000Z", [], { parentId: null }), [
+ siblingIssue("2", "2026-04-02T00:00:00.000Z", [], { parentId: null }),
+ ])).toBeNull();
+ expect(buildIssueSiblingNavigation(siblingIssue("1", "2026-04-01T00:00:00.000Z", [], { hiddenAt: new Date() }), []))
+ .toBeNull();
+ });
+
+ it("hides navigation when the current issue is the only visible child", () => {
+ const current = siblingIssue("1", "2026-04-01T00:00:00.000Z");
+ const navigation = buildIssueSiblingNavigation(current, [
+ current,
+ siblingIssue("2", "2026-04-02T00:00:00.000Z", [], { hiddenAt: new Date() }),
+ ]);
+
+ expect(navigation).toBeNull();
+ });
+
+ it("returns only next for the first sibling and only previous for the last sibling", () => {
+ const first = siblingIssue("1", "2026-04-01T00:00:00.000Z");
+ const last = siblingIssue("3", "2026-04-03T00:00:00.000Z");
+ const siblings = [
+ siblingIssue("2", "2026-04-02T00:00:00.000Z"),
+ last,
+ first,
+ ];
+
+ expect(buildIssueSiblingNavigation(first, siblings)).toMatchObject({
+ previous: null,
+ next: { id: "2" },
+ });
+ expect(buildIssueSiblingNavigation(last, siblings)).toMatchObject({
+ previous: { id: "2" },
+ next: null,
+ });
+ });
+
+ it("uses the first direct child as next when a root issue has no sibling next", () => {
+ const current = siblingIssue("1", "2026-04-01T00:00:00.000Z", [], { parentId: null });
+ const navigation = buildIssueSiblingNavigation(current, [], [
+ siblingIssue("3", "2026-04-03T00:00:00.000Z", ["2"], { parentId: "1" }),
+ siblingIssue("2", "2026-04-02T00:00:00.000Z", [], { parentId: "1" }),
+ ]);
+
+ expect(navigation).toMatchObject({
+ previous: null,
+ next: { id: "2" },
+ });
+ });
+
+ it("uses the first direct child as next when the current sibling is last", () => {
+ const current = siblingIssue("2", "2026-04-02T00:00:00.000Z");
+ const navigation = buildIssueSiblingNavigation(current, [
+ siblingIssue("1", "2026-04-01T00:00:00.000Z"),
+ current,
+ ], [
+ siblingIssue("4", "2026-04-04T00:00:00.000Z", ["3"], { parentId: "2" }),
+ siblingIssue("3", "2026-04-03T00:00:00.000Z", [], { parentId: "2" }),
+ ]);
+
+ expect(navigation).toMatchObject({
+ previous: { id: "1" },
+ next: { id: "3" },
+ });
+ });
+});
diff --git a/ui/src/lib/issue-detail-subissues.ts b/ui/src/lib/issue-detail-subissues.ts
index 379ca8c1..40274675 100644
--- a/ui/src/lib/issue-detail-subissues.ts
+++ b/ui/src/lib/issue-detail-subissues.ts
@@ -17,6 +17,13 @@ export type SubIssueProgressSummary = {
target: SubIssueProgressTarget | null;
};
+export type IssueSiblingNavigation = {
+ previous: Issue | null;
+ next: Issue | null;
+ currentIndex: number;
+ totalCount: number;
+};
+
export function shouldRenderRichSubIssuesSection(childIssuesLoading: boolean, childIssueCount: number): boolean {
return childIssuesLoading || childIssueCount > 0;
}
@@ -56,6 +63,57 @@ export function buildSubIssueProgressSummary(issues: Issue[]): SubIssueProgressS
};
}
+export function buildIssueSiblingNavigation(
+ currentIssue: Issue,
+ siblingIssues: Issue[],
+ childIssues: Issue[] = [],
+): IssueSiblingNavigation | null {
+ if (currentIssue.hiddenAt) return null;
+
+ const byId = new Map();
+ if (currentIssue.parentId) {
+ for (const issue of siblingIssues) {
+ if (issue.parentId !== currentIssue.parentId || issue.hiddenAt) continue;
+ byId.set(
+ issue.id,
+ issue.id === currentIssue.id
+ ? { ...issue, ...currentIssue, blockedBy: currentIssue.blockedBy ?? issue.blockedBy }
+ : issue,
+ );
+ }
+ if (!byId.has(currentIssue.id)) byId.set(currentIssue.id, currentIssue);
+ }
+
+ const ordered = workflowSort(Array.from(byId.values()));
+ const currentIndex = ordered.findIndex((issue) => issue.id === currentIssue.id);
+ const directChildren = workflowSort(
+ childIssues.filter((issue) => issue.parentId === currentIssue.id && !issue.hiddenAt),
+ );
+ const firstChild = directChildren[0] ?? null;
+
+ if (currentIndex < 0) {
+ return firstChild
+ ? {
+ previous: null,
+ next: firstChild,
+ currentIndex: 0,
+ totalCount: directChildren.length + 1,
+ }
+ : null;
+ }
+
+ const previous = currentIndex > 0 ? ordered[currentIndex - 1] : null;
+ const next = currentIndex < ordered.length - 1 ? ordered[currentIndex + 1] : firstChild;
+ if (!previous && !next) return null;
+
+ return {
+ previous,
+ next,
+ currentIndex,
+ totalCount: ordered.length,
+ };
+}
+
function isActionableStatus(status: IssueStatus): boolean {
return status !== "done" && status !== "cancelled" && status !== "blocked";
}
diff --git a/ui/src/lib/router.tsx b/ui/src/lib/router.tsx
index 0ec23ede..48891144 100644
--- a/ui/src/lib/router.tsx
+++ b/ui/src/lib/router.tsx
@@ -46,10 +46,19 @@ export * from "react-router-dom";
type CompanyLinkProps = React.ComponentProps & {
disableIssueQuicklook?: boolean;
issuePrefetch?: Issue | null;
+ issueQuicklookSide?: React.ComponentProps["issueQuicklookSide"];
+ issueQuicklookAlign?: React.ComponentProps["issueQuicklookAlign"];
};
export const Link = React.forwardRef(
- function CompanyLink({ to, disableIssueQuicklook = false, issuePrefetch = null, ...props }, ref) {
+ function CompanyLink({
+ to,
+ disableIssueQuicklook = false,
+ issuePrefetch = null,
+ issueQuicklookSide,
+ issueQuicklookAlign,
+ ...props
+ }, ref) {
const companyPrefix = useActiveCompanyPrefix();
const resolvedTo = resolveTo(to, companyPrefix);
const issuePathId = parseIssuePathIdFromPath(typeof resolvedTo === "string" ? resolvedTo : resolvedTo.pathname);
@@ -62,6 +71,8 @@ export const Link = React.forwardRef(
issuePathId={issuePathId}
disableIssueQuicklook={disableIssueQuicklook}
issuePrefetch={issuePrefetch}
+ issueQuicklookSide={issueQuicklookSide}
+ issueQuicklookAlign={issueQuicklookAlign}
{...props}
/>
);
diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx
index e4dd4c37..98f611d1 100644
--- a/ui/src/pages/IssueDetail.test.tsx
+++ b/ui/src/pages/IssueDetail.test.tsx
@@ -2,7 +2,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Agent, Issue, IssueTreeControlPreview, IssueTreeHold } from "@paperclipai/shared";
-import { act, type ButtonHTMLAttributes, type ReactNode } from "react";
+import { act, type AnchorHTMLAttributes, type ButtonHTMLAttributes, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { canBoardResolveRecoveryAction, IssueDetail } from "./IssueDetail";
@@ -110,7 +110,24 @@ vi.mock("../api/instanceSettings", () => ({
}));
vi.mock("@/lib/router", () => ({
- Link: ({ children, to }: { children?: ReactNode; to: string }) => {children},
+ Link: ({
+ children,
+ to,
+ state: _state,
+ issuePrefetch: _issuePrefetch,
+ issueQuicklookSide: _issueQuicklookSide,
+ issueQuicklookAlign: _issueQuicklookAlign,
+ ...props
+ }: {
+ children?: ReactNode;
+ to: string;
+ state?: unknown;
+ issuePrefetch?: unknown;
+ issueQuicklookSide?: unknown;
+ issueQuicklookAlign?: unknown;
+ } & AnchorHTMLAttributes) => (
+ {children}
+ ),
useLocation: () => ({ pathname: "/issues/PAP-1", search: "", hash: "", state: null }),
useNavigate: () => mockNavigate,
useNavigationType: () => "PUSH",
@@ -197,6 +214,7 @@ vi.mock("../components/IssueChatThread", () => ({
onStopRun?: (runId: string) => Promise;
stopRunLabel?: string;
stoppingRunLabel?: string;
+ footer?: ReactNode;
}) => {
mockIssueChatThreadRender(props);
return (
@@ -207,6 +225,7 @@ vi.mock("../components/IssueChatThread", () => ({
{props.stopRunLabel ?? "Stop run"}
) : null}
+ {props.footer}
);
},
@@ -839,6 +858,116 @@ describe("IssueDetail", () => {
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
+ it("renders sibling previous and next navigation at the chat footer", async () => {
+ const issue = createIssue({
+ id: "issue-2",
+ identifier: "PAP-2",
+ issueNumber: 2,
+ parentId: "parent-1",
+ title: "Current sibling",
+ createdAt: new Date("2026-04-02T00:00:00.000Z"),
+ });
+ const previous = createIssue({
+ id: "issue-1",
+ identifier: "PAP-1",
+ issueNumber: 1,
+ parentId: "parent-1",
+ title: "Previous sibling",
+ status: "done",
+ createdAt: new Date("2026-04-01T00:00:00.000Z"),
+ });
+ const next = createIssue({
+ id: "issue-3",
+ identifier: "PAP-3",
+ issueNumber: 3,
+ parentId: "parent-1",
+ title: "Next sibling",
+ blockedBy: [{ id: "issue-2" }] as Issue["blockedBy"],
+ createdAt: new Date("2026-04-03T00:00:00.000Z"),
+ });
+
+ mockIssuesApi.get.mockResolvedValue(issue);
+ mockIssuesApi.list.mockImplementation((_companyId, filters?: { descendantOf?: string; parentId?: string }) => {
+ if (filters?.parentId === "parent-1") return Promise.resolve([next, previous, issue]);
+ return Promise.resolve([]);
+ });
+
+ await act(async () => {
+ root.render(
+