Skip to content

Commit bdf1ff7

Browse files
authored
Rebuild Not actionable inbox tab on latest master
Master independently landed the resolved→Archive half of this PR (excluded statuses, isDismissedReport, the resolved status/labels/badge, the Archive query, resolved Restore-hiding in ReportCard/DismissedReportDetail, and the resolved→Archive detail redirect). Re-applying only the still-unique part on top of the moved-on master to clear the merge conflicts: - Add the staff-only Not actionable tab (gated on user.is_staff via INBOX_STAFF_ONLY_TAB_KEYS / isStaffOnlyInboxTab), listing reports the actionability judgment marked not_actionable. - isNotActionableReport mirrors isReportTabReport's gate order (excludes excluded/failed/PR-bearing/in-flight runs) so a report can't double-list in both Runs and Not actionable. - isReportTabReport excludes not-actionable reports so they don't also show in Reports. - Route + detail wiring: /code/inbox/not-actionable list + $reportId detail, ReportCard detailTab, ReportDetail backTo/backLabel, gate + frame + prefetch route unions, regenerated routeTree.gen.ts. - Tests for isNotActionableReport (incl. the in-flight/failed guards) and isStaffOnlyInboxTab; CLAUDE.md updated to a five-tab IA. Generated-By: PostHog Code Task-Id: 3a7dec81-6f1d-425a-a7cb-f468c36b3ec7
1 parent 518d429 commit bdf1ff7

14 files changed

Lines changed: 380 additions & 27 deletions

packages/core/src/inbox/reportMembership.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
isDismissedReport,
1111
isExcludedFromInbox,
1212
isInboxDetailPath,
13+
isNotActionableReport,
1314
isPullRequestReport,
1415
isReportTabReport,
16+
isStaffOnlyInboxTab,
1517
matchesReviewerScope,
1618
partitionRunsTabReports,
1719
teammateInboxScope,
@@ -58,10 +60,85 @@ describe("isDismissedReport", () => {
5860
});
5961
});
6062

63+
describe("isNotActionableReport", () => {
64+
it("matches reports the judgment marked not_actionable", () => {
65+
expect(
66+
isNotActionableReport(fakeReport({ actionability: "not_actionable" })),
67+
).toBe(true);
68+
});
69+
70+
it.each(["immediately_actionable", "requires_human_input", null] as const)(
71+
"does not match %s actionability",
72+
(actionability) => {
73+
expect(isNotActionableReport(fakeReport({ actionability }))).toBe(false);
74+
},
75+
);
76+
77+
it.each(["suppressed", "resolved"] as const)(
78+
"excludes terminal %s reports",
79+
(status) => {
80+
expect(
81+
isNotActionableReport(
82+
fakeReport({ actionability: "not_actionable", status }),
83+
),
84+
).toBe(false);
85+
},
86+
);
87+
88+
it("excludes PR-bearing reports", () => {
89+
expect(
90+
isNotActionableReport(
91+
fakeReport({
92+
actionability: "not_actionable",
93+
implementation_pr_url: "https://gh/p/1",
94+
}),
95+
),
96+
).toBe(false);
97+
});
98+
99+
it.each(["potential", "candidate", "in_progress", "pending_input"] as const)(
100+
"excludes in-flight %s runs (they stay in the Runs tab)",
101+
(status) => {
102+
expect(
103+
isNotActionableReport(
104+
fakeReport({ status, actionability: "not_actionable" }),
105+
),
106+
).toBe(false);
107+
},
108+
);
109+
110+
it("excludes failed reports (they stay in the Runs tab)", () => {
111+
expect(
112+
isNotActionableReport(
113+
fakeReport({ status: "failed", actionability: "not_actionable" }),
114+
),
115+
).toBe(false);
116+
});
117+
118+
it("matches a settled (ready) not_actionable report", () => {
119+
expect(
120+
isNotActionableReport(
121+
fakeReport({ status: "ready", actionability: "not_actionable" }),
122+
),
123+
).toBe(true);
124+
});
125+
});
126+
127+
describe("isStaffOnlyInboxTab", () => {
128+
it("gates only the Not actionable tab", () => {
129+
expect(isStaffOnlyInboxTab("not-actionable")).toBe(true);
130+
expect(isStaffOnlyInboxTab("reports")).toBe(false);
131+
expect(isStaffOnlyInboxTab("pulls")).toBe(false);
132+
expect(isStaffOnlyInboxTab("runs")).toBe(false);
133+
expect(isStaffOnlyInboxTab("dismissed")).toBe(false);
134+
});
135+
});
136+
61137
describe("isInboxDetailPath", () => {
62138
it("matches detail paths for each inbox tab", () => {
63139
expect(isInboxDetailPath("/code/inbox/pulls/abc")).toBe(true);
64140
expect(isInboxDetailPath("/code/inbox/reports/abc")).toBe(true);
141+
expect(isInboxDetailPath("/code/inbox/not-actionable/abc")).toBe(true);
65142
expect(isInboxDetailPath("/code/inbox/runs/abc")).toBe(true);
66143
});
67144

@@ -236,6 +313,14 @@ describe("tabFilters", () => {
236313
),
237314
).toBe(false);
238315
});
316+
317+
it("excludes not-actionable reports (they go to the Not actionable tab)", () => {
318+
expect(
319+
isReportTabReport(
320+
fakeReport({ status: "ready", actionability: "not_actionable" }),
321+
),
322+
).toBe(false);
323+
});
239324
});
240325

241326
describe("matchesReviewerScope", () => {

packages/core/src/inbox/reportMembership.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,43 @@ export function countInboxScopeReports(
8282
return reports.filter((report) => matchesInboxScope(report, scope)).length;
8383
}
8484

85-
export type InboxTabKey = "pulls" | "reports" | "runs" | "dismissed";
85+
export type InboxTabKey =
86+
| "pulls"
87+
| "reports"
88+
| "not-actionable"
89+
| "runs"
90+
| "dismissed";
8691

8792
export const INBOX_TAB_KEYS: InboxTabKey[] = [
8893
"pulls",
8994
"reports",
95+
"not-actionable",
9096
"runs",
9197
"dismissed",
9298
];
9399

94100
export const INBOX_TAB_LABEL: Record<InboxTabKey, string> = {
95101
pulls: "Pull requests",
96102
reports: "Reports",
103+
"not-actionable": "Not actionable",
97104
runs: "Runs",
98105
dismissed: "Archive",
99106
};
100107

108+
/**
109+
* Tabs only shown to staff (internal) users. Non-staff see Pull requests,
110+
* Reports, Runs and Archive. The Not actionable tab is an internal signal-quality
111+
* audit surface — reports the agent judged not worth acting on — so it stays
112+
* behind the staff flag, matching the PostHog Cloud inbox.
113+
*/
114+
export const INBOX_STAFF_ONLY_TAB_KEYS = new Set<InboxTabKey>([
115+
"not-actionable",
116+
]);
117+
118+
export function isStaffOnlyInboxTab(key: InboxTabKey): boolean {
119+
return INBOX_STAFF_ONLY_TAB_KEYS.has(key);
120+
}
121+
101122
/**
102123
* Canonical inbox tab list routes. Use these constants instead of hard-coding
103124
* `/code/inbox/pulls` etc., so renames stay in one place.
@@ -112,6 +133,7 @@ export const INBOX_TAB_LIST_ROUTE: Record<
112133
> = {
113134
pulls: "/code/inbox/pulls",
114135
reports: "/code/inbox/reports",
136+
"not-actionable": "/code/inbox/not-actionable",
115137
runs: "/code/inbox/runs",
116138
dismissed: "/code/inbox/dismissed",
117139
};
@@ -231,9 +253,31 @@ export function isReportTabReport(report: SignalReport): boolean {
231253
// rather than reappearing as a Report.
232254
if (report.implementation_pr_url) return false;
233255
if (isAgentRunReport(report)) return false;
256+
// Reports the agent judged not worth acting on get their own (staff-only)
257+
// tab and are kept out of the main Reports list, matching the Cloud inbox.
258+
if (isNotActionableReport(report)) return false;
234259
return true;
235260
}
236261

262+
/**
263+
* Not-actionable tab membership: reports the agentic actionability judgment
264+
* marked `not_actionable`. A staff-only signal-quality audit surface. These are
265+
* still in-pipeline reports (the judgment is an artefact, not a status), so this
266+
* partitions the same main list the other report tabs read — it just keeps them
267+
* out of the Reports tab via the check in `isReportTabReport`.
268+
*/
269+
export function isNotActionableReport(report: SignalReport): boolean {
270+
if (isExcludedFromInbox(report)) return false;
271+
if (report.status === "failed") return false; // failed runs live in the Runs tab only
272+
if (report.implementation_pr_url) return false;
273+
// In-flight (queued/live) runs belong to the Runs tab until they settle, even
274+
// if a not_actionable judgment is already attached. Mirror the gate order in
275+
// `isReportTabReport` so the two predicates classify a report the same way and
276+
// it can't show in both the Runs and Not actionable tabs at once.
277+
if (isAgentRunReport(report)) return false;
278+
return report.actionability === "not_actionable";
279+
}
280+
237281
export function matchesReviewerScope(
238282
report: SignalReport,
239283
scope: InboxScope,

packages/ui/src/features/inbox/CLAUDE.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,24 @@ The main objects are:
1515

1616
## Information Architecture
1717

18-
Inbox has four tabs and one reviewer-scope control:
18+
Inbox has five tabs and one reviewer-scope control:
1919

2020
| Tab | Route | Membership |
2121
| --- | --- | --- |
2222
| Pull requests | `/code/inbox/pulls` | Reports with `implementation_pr_url` set |
23-
| Reports | `/code/inbox/reports` | Reports without a PR and not currently running |
23+
| Reports | `/code/inbox/reports` | Reports without a PR, not running, and not judged not-actionable |
24+
| Not actionable | `/code/inbox/not-actionable` | Reports the judgment marked `actionability === "not_actionable"` (**staff-only**) |
2425
| Runs | `/code/inbox/runs` | Reports that are still in progress or waiting on input |
2526
| Archive | `/code/inbox/dismissed` | Terminal reports: archived/suppressed (`status === "suppressed"`) and resolved-by-merged-PR (`status === "resolved"`) |
2627

28+
The **Not actionable** tab is gated to staff (internal) users via
29+
`INBOX_STAFF_ONLY_TAB_KEYS` (see `isStaffOnlyInboxTab`); `InboxTabBar` hides it
30+
for non-staff, keyed off `user.is_staff`. It's an internal signal-quality audit
31+
surface and mirrors the same staff-only tab in `PostHog/posthog`. It reuses the
32+
shared `InboxReportListTab` shell (same as Reports/Pulls); cards link to its own
33+
detail route so back-navigation stays on the tab. `isReportTabReport` excludes
34+
not-actionable reports so they don't double-list in Reports.
35+
2736
Detail pages live under the same tab: `/code/inbox/<tab>/$reportId`.
2837

2938
The Archive tab (route `/code/inbox/dismissed`, user-facing label "Archive") is
@@ -76,6 +85,7 @@ The tab components are intentionally simple:
7685

7786
- `PullRequestsTab` partitions scoped reports with `isPullRequestReport`.
7887
- `ReportsTab` partitions with `isReportTabReport`.
88+
- `NotActionableTab` (staff-only) partitions with `isNotActionableReport`.
7989
- `RunsTab` partitions with `isAgentRunReport`.
8090
- `DismissedTab` (the "Archive" tab) lists its own `useInboxDismissedReports` query (matching `isDismissedReport`); read-only detail route, restore action per card.
8191

packages/ui/src/features/inbox/components/InboxDetailFrame.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ import type { ComponentType, ReactNode } from "react";
2727
interface InboxDetailFrameProps {
2828
report: SignalReport;
2929
/** List route for the back-link (e.g. "/code/inbox/pulls"). */
30-
backTo: "/code/inbox/pulls" | "/code/inbox/reports" | "/code/inbox/dismissed";
30+
backTo:
31+
| "/code/inbox/pulls"
32+
| "/code/inbox/reports"
33+
| "/code/inbox/not-actionable"
34+
| "/code/inbox/dismissed";
3135
backLabel: string;
3236
/**
3337
* Whether to render the Dismiss button + dialog. Off for already-dismissed

packages/ui/src/features/inbox/components/InboxReportDetailGate.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
isDismissedReport,
3+
isNotActionableReport,
34
isPullRequestReport,
45
isReportTabReport,
56
} from "@posthog/core/inbox/reportMembership";
@@ -21,6 +22,7 @@ interface InboxReportDetailGateProps {
2122
backTo:
2223
| "/code/inbox/pulls"
2324
| "/code/inbox/reports"
25+
| "/code/inbox/not-actionable"
2426
| "/code/inbox/runs"
2527
| "/code/inbox/dismissed";
2628
backLabel: string;
@@ -31,6 +33,7 @@ interface InboxReportDetailGateProps {
3133
type InboxDetailRoute =
3234
| "/code/inbox/pulls/$reportId"
3335
| "/code/inbox/reports/$reportId"
36+
| "/code/inbox/not-actionable/$reportId"
3437
| "/code/inbox/runs/$reportId"
3538
| "/code/inbox/dismissed/$reportId";
3639

@@ -43,6 +46,8 @@ type InboxDetailRoute =
4346
*/
4447
function nonSuppressedDetailRoute(report: SignalReport): InboxDetailRoute {
4548
if (isPullRequestReport(report)) return "/code/inbox/pulls/$reportId";
49+
if (isNotActionableReport(report))
50+
return "/code/inbox/not-actionable/$reportId";
4651
if (isReportTabReport(report)) return "/code/inbox/reports/$reportId";
4752
return "/code/inbox/runs/$reportId";
4853
}
@@ -165,7 +170,11 @@ function tabFromBackTo(
165170
): InboxDetailTab | null {
166171
if (backTo === "/code/inbox/pulls") return "pulls";
167172
if (backTo === "/code/inbox/runs") return "runs";
173+
// Archive and the staff-only Not actionable tab aren't part of the triage
174+
// funnel, so their detail opens aren't tracked (rank would be measured against
175+
// the wrong list).
168176
if (backTo === "/code/inbox/dismissed") return null;
177+
if (backTo === "/code/inbox/not-actionable") return null;
169178
return "reports";
170179
}
171180

packages/ui/src/features/inbox/components/InboxTabBar.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
INBOX_TAB_LIST_ROUTE,
55
type InboxTabCounts,
66
type InboxTabKey,
7+
isStaffOnlyInboxTab,
78
} from "@posthog/core/inbox/reportMembership";
89
import { Tabs, TabsList, TabsTrigger } from "@posthog/quill";
10+
import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery";
911
import { InboxScopeSelect } from "@posthog/ui/features/inbox/components/InboxScopeSelect";
1012
import { Flex } from "@radix-ui/themes";
1113
import { useNavigate, useRouterState } from "@tanstack/react-router";
@@ -15,6 +17,11 @@ interface InboxTabBarProps {
1517
}
1618

1719
function activeTabFromPath(pathname: string): InboxTabKey {
20+
// Check "not-actionable" before "reports": the former is not a prefix of the
21+
// latter, but keeping the more specific routes first guards against future
22+
// overlaps.
23+
if (pathname.startsWith(INBOX_TAB_LIST_ROUTE["not-actionable"]))
24+
return "not-actionable";
1825
if (pathname.startsWith(INBOX_TAB_LIST_ROUTE.reports)) return "reports";
1926
if (pathname.startsWith(INBOX_TAB_LIST_ROUTE.runs)) return "runs";
2027
if (pathname.startsWith(INBOX_TAB_LIST_ROUTE.dismissed)) return "dismissed";
@@ -25,6 +32,11 @@ export function InboxTabBar({ counts }: InboxTabBarProps) {
2532
const navigate = useNavigate();
2633
const pathname = useRouterState({ select: (s) => s.location.pathname });
2734
const activeKey = activeTabFromPath(pathname);
35+
const { data: currentUser } = useMeQuery();
36+
const isStaff = currentUser?.is_staff === true;
37+
const visibleTabKeys = INBOX_TAB_KEYS.filter(
38+
(key) => isStaff || !isStaffOnlyInboxTab(key),
39+
);
2840

2941
return (
3042
<Flex align="center" justify="between" className="min-w-0">
@@ -39,7 +51,7 @@ export function InboxTabBar({ counts }: InboxTabBarProps) {
3951
variant="line"
4052
className="h-auto gap-0.5 [&_.quill-tabs__indicator]:transition-[transform,width]! [&_.quill-tabs__indicator]:duration-100! [&_.quill-tabs__indicator]:ease-out!"
4153
>
42-
{INBOX_TAB_KEYS.map((key) => {
54+
{visibleTabKeys.map((key) => {
4355
const isActive = key === activeKey;
4456
return (
4557
<TabsTrigger
@@ -50,18 +62,21 @@ export function InboxTabBar({ counts }: InboxTabBarProps) {
5062
<span className="font-medium text-[13px]">
5163
{INBOX_TAB_LABEL[key]}
5264
</span>
53-
{/* Runs and the open-ended Archive don't get a running total — it adds no signal. */}
54-
{key !== "runs" && key !== "dismissed" && counts[key] > 0 && (
55-
<span
56-
className={
57-
isActive
58-
? "text-[12px] text-gray-11 tabular-nums"
59-
: "text-[12px] text-gray-10 tabular-nums"
60-
}
61-
>
62-
{counts[key]}
63-
</span>
64-
)}
65+
{/* Runs, Not actionable, and the open-ended Archive don't get a running total — it adds no signal. */}
66+
{key !== "runs" &&
67+
key !== "dismissed" &&
68+
key !== "not-actionable" &&
69+
counts[key] > 0 && (
70+
<span
71+
className={
72+
isActive
73+
? "text-[12px] text-gray-11 tabular-nums"
74+
: "text-[12px] text-gray-10 tabular-nums"
75+
}
76+
>
77+
{counts[key]}
78+
</span>
79+
)}
6580
</TabsTrigger>
6681
);
6782
})}

0 commit comments

Comments
 (0)