diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx
index bcede8f1dd..6d1027ba9f 100644
--- a/ui/src/components/IssueRow.tsx
+++ b/ui/src/components/IssueRow.tsx
@@ -61,6 +61,38 @@ interface IssueRowProps {
showDivider?: boolean;
}
+export function InboxArchiveButton({
+ onArchive,
+ disabled,
+}: {
+ onArchive: () => void;
+ disabled?: boolean;
+}) {
+ return (
+
+ );
+}
+
export function IssueRow({
issue,
issueLinkState,
@@ -292,27 +324,7 @@ export function IssueRow({
{(onArchive || desktopTrailing || trailingMeta || externalObjectSummary) ? (
{onArchive ? (
-
+
) : null}
{externalObjectSummary ? (
diff --git a/ui/src/components/MarkdownCodeBlockStyles.test.ts b/ui/src/components/MarkdownCodeBlockStyles.test.ts
index f269e90bf8..13f31b82e4 100644
--- a/ui/src/components/MarkdownCodeBlockStyles.test.ts
+++ b/ui/src/components/MarkdownCodeBlockStyles.test.ts
@@ -18,8 +18,8 @@ function cssBlock(selector: string): string {
return stylesheet.slice(bodyStart + 1, bodyEnd);
}
-/* The rendered code block used to be pinned to the Catppuccin literals
- #1e1e2e / #cdd6f4, in the normal AND the prose-invert variables. A code
+/* The rendered code block used to be pinned to fixed Catppuccin literals
+ in the normal AND the prose-invert variables. A code
block therefore stayed dark in light mode. These tests fail if any of
those surfaces is pinned to a literal again, rather than riding a token
that carries a `.dark` override. */
diff --git a/ui/src/pages/Inbox.test.tsx b/ui/src/pages/Inbox.test.tsx
index 4e5cb53bb5..b7445fa50e 100644
--- a/ui/src/pages/Inbox.test.tsx
+++ b/ui/src/pages/Inbox.test.tsx
@@ -4,7 +4,7 @@ import type { ComponentProps } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import type { Issue } from "@paperclipai/shared";
+import type { Approval, HeartbeatRun, Issue } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CompanyJoinRequest } from "../api/access";
import {
@@ -266,6 +266,73 @@ function createJoinRequest(
};
}
+function createApproval(overrides: Partial = {}): Approval {
+ return {
+ id: "approval-1",
+ companyId: "company-1",
+ type: "hire_agent",
+ requestedByAgentId: null,
+ requestedByUserId: "local-board",
+ status: "pending",
+ payload: { name: "New teammate" },
+ decisionNote: null,
+ decidedByUserId: null,
+ decidedAt: null,
+ createdAt: new Date("2026-03-11T00:00:00.000Z"),
+ updatedAt: new Date("2026-03-11T00:00:00.000Z"),
+ ...overrides,
+ };
+}
+
+function createFailedRun(overrides: Partial = {}): HeartbeatRun {
+ return {
+ id: "run-1",
+ companyId: "company-1",
+ agentId: "agent-1",
+ responsibleUserId: null,
+ invocationSource: "assignment",
+ triggerDetail: null,
+ status: "failed",
+ error: "boom",
+ wakeupRequestId: null,
+ exitCode: null,
+ signal: null,
+ usageJson: null,
+ resultJson: null,
+ sessionIdBefore: null,
+ sessionIdAfter: null,
+ logStore: null,
+ logRef: null,
+ logBytes: null,
+ logSha256: null,
+ logCompressed: false,
+ stdoutExcerpt: null,
+ stderrExcerpt: null,
+ errorCode: null,
+ externalRunId: null,
+ processPid: null,
+ processGroupId: null,
+ processStartedAt: null,
+ lastOutputAt: null,
+ lastOutputSeq: 0,
+ lastOutputStream: null,
+ lastOutputBytes: null,
+ retryOfRunId: null,
+ processLossRetryCount: 0,
+ livenessState: null,
+ livenessReason: null,
+ continuationAttempt: 0,
+ lastUsefulActionAt: null,
+ nextAction: null,
+ contextSnapshot: null,
+ startedAt: new Date("2026-03-11T00:00:00.000Z"),
+ finishedAt: new Date("2026-03-11T00:01:00.000Z"),
+ createdAt: new Date("2026-03-11T00:00:00.000Z"),
+ updatedAt: new Date("2026-03-11T00:01:00.000Z"),
+ ...overrides,
+ };
+}
+
function resetInboxApiMocks() {
for (const mock of Object.values(apiMocks)) mock.mockReset();
externalObjectMocks.summaries.clear();
@@ -345,6 +412,49 @@ describe("Inbox toolbar", () => {
act(() => root.unmount());
});
+ it("keeps archive hover actions and swipe targets on every unread non-task Mine row", async () => {
+ routerMock.location.pathname = "/inbox/mine";
+ localStorage.setItem("paperclip:inbox:group-by", "none");
+ apiMocks.approvalsList.mockResolvedValue([createApproval()]);
+ apiMocks.heartbeatRunsList.mockResolvedValue([createFailedRun()]);
+ apiMocks.joinRequestsList.mockResolvedValue([createJoinRequest()]);
+
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
+ });
+ const root = createRoot(container);
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await vi.waitFor(() => {
+ expect(container.textContent).toContain("Hire Agent: New teammate");
+ expect(container.textContent).toContain("Failed run");
+ expect(container.textContent).toContain("Jordan Example");
+ });
+
+ const rowFor = (text: string) =>
+ [...container.querySelectorAll("[data-inbox-item]")]
+ .find((row) => row.textContent?.includes(text));
+
+ for (const text of ["Hire Agent: New teammate", "Failed run", "Jordan Example"]) {
+ const row = rowFor(text);
+ expect(row, `missing inbox row for ${text}`).toBeDefined();
+ expect(row?.querySelector('button[aria-label="Mark as read"]')).not.toBeNull();
+ const archiveButton = row?.querySelector('button[aria-label="Archive"]');
+ expect(archiveButton).not.toBeNull();
+ expect(archiveButton?.className).toContain("opacity-0");
+ expect(archiveButton?.className).toContain("group-hover:opacity-100");
+ expect(row?.querySelector("[data-inbox-row-surface]")).not.toBeNull();
+ }
+
+ act(() => root.unmount());
+ });
+
it("restores folded and unfolded sub-tasks across remounts", async () => {
routerMock.location.pathname = "/inbox/mine";
const storageKey = "paperclip:inbox:collapsed-parents:company-1";
@@ -990,51 +1100,7 @@ describe("FailedRunInboxRow", () => {
it("suppresses accent hover styling when selected", () => {
const root = createRoot(container);
- const run = {
- id: "run-1",
- companyId: "company-1",
- agentId: "agent-1",
- responsibleUserId: null,
- invocationSource: "assignment",
- triggerDetail: null,
- status: "failed",
- error: "boom",
- wakeupRequestId: null,
- exitCode: null,
- signal: null,
- usageJson: null,
- resultJson: null,
- sessionIdBefore: null,
- sessionIdAfter: null,
- logStore: null,
- logRef: null,
- logBytes: null,
- logSha256: null,
- logCompressed: false,
- lastOutputAt: null,
- lastOutputSeq: 0,
- lastOutputStream: null,
- lastOutputBytes: null,
- errorCode: null,
- externalRunId: null,
- processPid: null,
- processGroupId: null,
- processStartedAt: null,
- retryOfRunId: null,
- processLossRetryCount: 0,
- livenessState: null,
- livenessReason: null,
- continuationAttempt: 0,
- lastUsefulActionAt: null,
- nextAction: null,
- stdoutExcerpt: null,
- stderrExcerpt: null,
- contextSnapshot: null,
- startedAt: new Date("2026-03-11T00:00:00.000Z"),
- finishedAt: null,
- createdAt: new Date("2026-03-11T00:00:00.000Z"),
- updatedAt: new Date("2026-03-11T00:00:00.000Z"),
- } as const;
+ const run = createFailedRun();
act(() => {
root.render(
diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx
index 4bf93eb0d4..1d2fa37c62 100644
--- a/ui/src/pages/Inbox.tsx
+++ b/ui/src/pages/Inbox.tsx
@@ -79,7 +79,7 @@ import {
issueTrailingColumns,
} from "../components/IssueColumns";
import { IssueFiltersPopover } from "../components/IssueFiltersPopover";
-import { IssueRow } from "../components/IssueRow";
+import { InboxArchiveButton, IssueRow } from "../components/IssueRow";
import { BlockedInboxView } from "../components/BlockedInboxView";
import { SwipeToArchive } from "../components/SwipeToArchive";
@@ -340,16 +340,6 @@ export function FailedRunInboxRow({
unreadState === "fading" ? "opacity-0" : "opacity-100",
)} />
- ) : onArchive ? (
-
) : (
)}
@@ -389,6 +379,9 @@ export function FailedRunInboxRow({
+ {onArchive ? (
+
+ ) : null}
- ) : onArchive ? (
-
) : (
)}
@@ -534,25 +517,32 @@ function ApprovalInboxRow({
- {showResolutionButtons ? (
+ {(onArchive || showResolutionButtons) ? (
-
-
+ {onArchive ? (
+
+ ) : null}
+ {showResolutionButtons ? (
+ <>
+
+
+ >
+ ) : null}
) : null}
@@ -632,16 +622,6 @@ function JoinRequestInboxRow({
unreadState === "fading" ? "opacity-0" : "opacity-100",
)} />
- ) : onArchive ? (
-
) : (
)}
@@ -664,6 +644,9 @@ function JoinRequestInboxRow({
+ {onArchive ? (
+
+ ) : null}