Skip to content

feat(inbox): add Dismissed tab with restore#2704

Merged
Twixes merged 11 commits into
mainfrom
inbox/dismissed-tab
Jun 18, 2026
Merged

feat(inbox): add Dismissed tab with restore#2704
Twixes merged 11 commits into
mainfrom
inbox/dismissed-tab

Conversation

@andrewm4894

@andrewm4894 andrewm4894 commented Jun 16, 2026

Copy link
Copy Markdown
Member

Adds a Dismissed tab to the inbox: lists dismissed (suppressed) reports, lets you re-read one, restore it, or grab a shareable link.

Backend dependency (PostHog/posthog#64019) is merged + deployed, and the whole feature has been verified end-to-end against the live app (including a real restore round-trip — status confirmed flipping suppressed → potential server-side).

What's in it

Tab + list

  • New 4th tab. useInboxDismissedReports fetches status=suppressed (the main pipeline query excludes suppressed reports), no polling — relies on reportKeys.all invalidation, which dismiss/restore both trigger.
  • No count badge (an open-ended archive total adds no signal, and skipping it avoids an always-on count query in the inbox shell). Reviewer-scope selector hidden (the dismissed list isn't scoped). Membership stays canonical in reportMembership.ts (isDismissedReport), unit-tested.

Restore

  • useInboxRestoreReport reuses the state action's potential ("reopen") transition — the only reopen path the backend exposes. Invalidates reportKeys.all so the report leaves Dismissed.
  • ⚠️ Behaviour worth knowing: restore sets the report to potential, which is a queued run status, so a restored report lands in the Runs tab (Queued), not Reports — it re-enters the pipeline rather than popping back to its prior state (that prior status isn't persisted, and the state endpoint only accepts potential/suppressed). It cycles back to Reports on its own once it re-reaches ready. It stays re-dismissible throughout (via the run → report detail, or from Reports once ready).

Read-only detail

  • DismissedReportDetail reuses InboxReportDetailGate + InboxDetailFrame — summary + evidence + Restore + copy-link. No triage affordances (dismiss / discuss / create PR / reviewers): the report is out of the pipeline until restored.
  • InboxDetailFrame gains a showDismiss opt-out; the gate skips OPENED/CLOSED engagement tracking for the dismissed tab (not a tracked InboxDetailTab — its rank would be measured against the wrong list).
  • Routes: dismissed.tsx is an Outlet layout, with dismissed.index (list) and dismissed.$reportId (detail). The loader resolves the cached report from the dismissed list cache, so navigation renders instantly.

Shareable link

  • Copy-link button beside Restore (same copyInboxReportLink deep-link util the other detail screens use).
  • useOpenInboxReport now routes a suppressed report to the Dismissed detail (it previously had no suppressed branch and fell through to the Reports detail, which showed a stray Dismiss action on an already-dismissed report). Adds navigateToInboxDismissedDetail.

Verification

  • @posthog/core + @posthog/ui typecheck ✅ · core tests 1550 passed ✅ · biome lint ✅ (pre-commit runs biome + full typecheck)
  • Driven in the running app over CDP: tab renders (no count badge, scope hidden), card → detail loads with summary + evidence, copy-link present beside Restore.
  • Restore round-trip confirmed server-side via the API: report 019eced2… went suppressed → potential.

Reviewer notes

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 4f3f02b.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff (1)

  1. packages/core/src/inbox/reportMembership.ts, line 109-111 (link)

    P2 isInboxDetailPath now matches phantom dismissed detail paths

    INBOX_DETAIL_PATH_RE is built from INBOX_TAB_KEYS, which now includes "dismissed". So /code/inbox/dismissed/<anyId> will be treated as a detail path by isInboxDetailPath, causing InboxView to hide InboxPageHeader even though no detail route exists for dismissed reports. A user who manually navigates to such a URL gets a router 404 with the page header stripped. Consider excluding "dismissed" from the regex, or building the pattern from a separate INBOX_DETAIL_TAB_KEYS constant that only lists tabs that actually have detail routes.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/core/src/inbox/reportMembership.ts
    Line: 109-111
    
    Comment:
    **`isInboxDetailPath` now matches phantom dismissed detail paths**
    
    `INBOX_DETAIL_PATH_RE` is built from `INBOX_TAB_KEYS`, which now includes `"dismissed"`. So `/code/inbox/dismissed/<anyId>` will be treated as a detail path by `isInboxDetailPath`, causing `InboxView` to hide `InboxPageHeader` even though no detail route exists for dismissed reports. A user who manually navigates to such a URL gets a router 404 with the page header stripped. Consider excluding `"dismissed"` from the regex, or building the pattern from a separate `INBOX_DETAIL_TAB_KEYS` constant that only lists tabs that actually have detail routes.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/core/src/inbox/reportMembership.test.ts:41-53
**Prefer `it.each` for the negative-status loop**

The `for...of` inside a single `it()` block will report only "does not match active or deleted reports" when any status fails, making it impossible to tell which status triggered the failure without reading the stack trace. Per the team's preference for parametrised tests, converting this to `it.each` gives one named case per status and immediate failure attribution.

### Issue 2 of 2
packages/core/src/inbox/reportMembership.ts:109-111
**`isInboxDetailPath` now matches phantom dismissed detail paths**

`INBOX_DETAIL_PATH_RE` is built from `INBOX_TAB_KEYS`, which now includes `"dismissed"`. So `/code/inbox/dismissed/<anyId>` will be treated as a detail path by `isInboxDetailPath`, causing `InboxView` to hide `InboxPageHeader` even though no detail route exists for dismissed reports. A user who manually navigates to such a URL gets a router 404 with the page header stripped. Consider excluding `"dismissed"` from the regex, or building the pattern from a separate `INBOX_DETAIL_TAB_KEYS` constant that only lists tabs that actually have detail routes.

Reviews (1): Last reviewed commit: "feat(inbox): add Dismissed tab with rest..." | Re-trigger Greptile

Comment thread packages/core/src/inbox/reportMembership.test.ts Outdated
@andrewm4894 andrewm4894 marked this pull request as ready for review June 16, 2026 15:26
@andrewm4894 andrewm4894 self-assigned this Jun 16, 2026
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "feat(inbox): add read-only detail view f..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e17a9f273d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/features/inbox/hooks/useInboxDismissedReports.ts
Comment thread packages/ui/src/features/inbox/components/DismissedTab.tsx
@andrewm4894 andrewm4894 force-pushed the inbox/dismissed-tab branch 2 times, most recently from 79333c6 to 4de74ab Compare June 17, 2026 09:16
@andrewm4894 andrewm4894 requested a review from a team June 17, 2026 11:00

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95690b4824

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/features/inbox/components/DismissedReportDetail.tsx
@andrewm4894

Copy link
Copy Markdown
Member Author

Added: the dismissed report cards now show why each report was suppressed — the dismissal reason as a chip (with the note as a tooltip).

The reason/note are denormalised onto the list SignalReport by the backend serializer in PostHog/posthog#64267 (same artefact-lift pattern as priority/actionability/already_addressed), so cards avoid an N+1 per-card artefact fetch. The field is optional — this PR ships independently; cards just show no chip until the backend lands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c06b7d57e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/features/inbox/components/DismissedReportDetail.tsx Outdated
Comment thread packages/ui/src/features/inbox/components/DismissedReportDetail.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f391b98053

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/features/inbox/components/DismissedReportDetail.tsx Outdated
Comment thread packages/ui/src/features/inbox/components/InboxReportDetailGate.tsx
Comment thread packages/ui/src/features/inbox/components/DismissedTab.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b870732fd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const onDismissedRoute = backTo === "/code/inbox/dismissed";
const isSuppressed = resolvedReport?.status === "suppressed";
let redirectTo: InboxDetailRoute | null = null;
if (resolvedReport && !isFetching) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide stale detail actions during status refetch

When a report is already cached as a normal inbox report but has been suppressed in another session, useInboxReportById returns that cached row while the forced fresh fetch is still pending. This branch only postpones the redirect while isFetching, but the component then falls through and renders children(resolvedReport), so /reports, /pulls, or /runs can briefly show full triage controls (including Create PR/Discuss) for a now-suppressed report until the fetch settles. Consider rendering the spinner or disabling actions while the route-boundary status is being confirmed.

Useful? React with 👍 / 👎.

Twixes commented Jun 18, 2026

Copy link
Copy Markdown
Member

🤖 Pushed 6c59a46 (via PostHog Code from a Slack thread) doing the rename + one review fix:

Dismiss/Dismissed → Archive (user-facing copy), aligning with the Cloud "Archive" wording:

  • "Dismissed" tab → Archive; card/detail/empty-state copy now says archived ("Archived {date}", "Back to archive", "No archived reports").
  • The Dismiss action → Archive: single-report button + dialog title/confirm ("Archive & teach the agent"), the card/detail tooltips and aria-labels ("Archive this report"), and the bulk-action toast.
  • The permanent-suppress option tooltip and the suppress success toast now read archive(d).

Internal vocabulary is deliberately unchanged — the route segment (/code/inbox/dismissed), query keys, component/hook/file names, and the backend status (suppressed) all stay, so analytics and deep links are stable. The distinct Snooze / Suppress bulk labels are left as-is (suppress carries the stronger "ignore future matching signals" semantics). Happy to fold those into "Archive" too if you'd prefer full alignment.

Addressed the open Codex comment ("Hide stale detail actions during status refetch"): on a triage route, InboxReportDetailGate now holds the spinner while isFetching && !isFetchedAfterMount, so the forced post-mount fetch confirms the status before any triage children (create-PR / discuss / archive) render — closing the brief window where a report suppressed in another session showed triage actions from a stale cache. The Archive route is excluded so it keeps its instant render-from-cache (read-only; Restore re-checks status server-side).

Typecheck (@posthog/ui + @posthog/core + @posthog/shared), biome, and the core/ui/shared inbox tests all pass locally.

Comment on lines +18 to +26
/**
* Dismissed tab membership: reports the user suppressed from the inbox. The
* dismiss action sets `suppressed`; `deleted` is terminal and never listed.
* These reports are fetched by a dedicated query (the main pipeline query
* excludes them), so this predicate is applied to that separate list.
*/
export function isDismissedReport(report: SignalReport): boolean {
return report.status === "suppressed";
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't quite get this - I would think dimissed (or archived), should include both dismissed, resolved, snoozed AND suppressed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — the short answer is there's only one status that fits. The SignalReportStatus enum is potential | candidate | in_progress | ready | failed | pending_input | suppressed | deleted — there is no separate dismissed or resolved status, the archive action just sets suppressed. The other not-in-inbox states don't belong here: deleted is permanent (gone, never restorable), and snooze isn't a status at all — it's a temporary snoozed_until timestamp on an otherwise-active report that auto-returns to the inbox when it elapses, so surfacing it in a manual restore list would be wrong. I've expanded the doc comment in e5ae02c to spell this out.

Comment on lines +28 to +31
/**
* Card for the Dismissed tab. Links into the read-only dismissed detail view;
* the Restore button (right column) stops propagation so it doesn't navigate.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels too custom, like it should just be a variant of the report card, fully handled by the report card component

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e5ae02cDismissedReportCard is gone, the Archive tab now renders <ReportCard variant="archived">. The variant shares all the card chrome and just swaps the triage actions for a single Restore button, shows the archive date + dismissal reason in the metadata row, links to the read-only archived detail view, and skips the artefact fetch it never renders. Net ~150 lines of duplication removed.

Comment on lines +15 to +20
/**
* Status filter for the Dismissed tab. Suppressed reports are excluded from the
* main pipeline query, so the Dismissed tab fetches them explicitly. `deleted`
* is terminal and stripped server-side, so it is never listed here.
*/
export const INBOX_DISMISSED_STATUS_FILTER = "suppressed";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned in the other comment, feels fishy only suppressed state is special here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as the isDismissedReport thread: suppressed is the only status that represents a user-archived report. deleted is stripped server-side and snooze is a timestamp rather than a status, so neither is a candidate here. Expanded the comment in e5ae02c to make the rationale explicit.

Twixes added a commit that referenced this pull request Jun 18, 2026
Address Michael's review on #2704:

- Replace the standalone DismissedReportCard with an `archived` variant of
  ReportCard, so the archive list reuses the same card chrome (priority
  monogram, title, headline, source meta, findings count) instead of
  duplicating ~150 lines. The variant swaps triage affordances for a single
  Restore action, shows the archive date + dismissal reason in the metadata
  row, links to the read-only archived detail view, and skips the artefact
  fetch it doesn't render.
- Clarify why `suppressed` is the only status the Archive tab lists: there is
  no separate dismissed/resolved status in the enum, `deleted` is permanent,
  and snooze is a temporary timestamp that auto-returns — none belong in a
  manual restore list.

Generated-By: PostHog Code
Task-Id: c0aa88e1-2b85-49c1-8f0e-d6b98f20b251
Twixes added 11 commits June 18, 2026 16:09
Adds a fourth inbox tab listing dismissed (suppressed) reports, newest first,
with a per-card Restore action that reopens a report back into the pipeline
(reuses the state action's 'potential' transition).

- Dedicated useInboxDismissedReports query (status=suppressed); the main
  pipeline query excludes suppressed, so the tab fetches them separately.
- useInboxRestoreReport invalidates reportKeys.all so a restored report moves
  out of Dismissed and back into the pipeline tabs.
- Cards are read-only (no detail link): the report detail endpoint currently
  404s for suppressed reports. Clicking-through depends on PostHog/posthog#64019
  shipping; until then Restore is the only action.
- No count badge on the tab (an open-ended archive total adds no signal), so
  no extra always-on query in the inbox shell.
- Reviewer-scope selector hidden on the tab (the dismissed list isn't scoped).

Updates the feature CLAUDE.md IA map to four tabs.
Dismissed cards now link into a detail view (summary + evidence) with a single
Restore action; no triage affordances since the report is out of the pipeline.

- DismissedReportDetail reuses InboxReportDetailGate + InboxDetailFrame.
- InboxDetailFrame gains a showDismiss opt-out (a dismissed report shouldn't
  show 'Dismiss'); the gate skips OPENED/CLOSED engagement tracking for the
  dismissed tab (its rank would be measured against the wrong list).
- Routes restructured: dismissed.tsx is now an Outlet layout, with
  dismissed.index (list) and dismissed.$reportId (detail).
- The route loader resolves the cached report from the dismissed list cache, so
  navigation renders instantly; the detail's own fetch depends on the backend
  serving suppressed reports on retrieve/signals (PostHog/posthog#64019).
- DismissedReportDetail gains a copy-link button beside Restore (same deep-link
  util the other detail screens use), so a dismissed report can be shared.
- useOpenInboxReport now routes a suppressed report to the Dismissed detail
  (was falling through to the Reports detail, which showed a stray Dismiss
  action on an already-dismissed report). Adds navigateToInboxDismissedDetail.
Surface why each report was suppressed: render the dismissal reason (with
the note as a tooltip) as a chip in the DismissedReportCard meta row.

The reason and note are denormalised onto the list SignalReport by the
backend serializer, mirroring how priority/actionability/already_addressed
are lifted from their artefacts, so cards avoid an N+1 per-card fetch.
Reason codes are client-owned, so an unknown value falls back to the raw
code via dismissalReasonLabel rather than being dropped.
A /code/inbox/dismissed/$reportId URL can go stale (history, bookmark, or
copied deep link) after the report was restored and moved on. The detail
view rendered Restore unconditionally, and READY/RESOLVED -> POTENTIAL is an
allowed server-side transition, so restoring re-queued an active report.

Redirect non-suppressed reports to their current home (pulls when a PR
exists, else reports), mirroring useOpenInboxReport's routing, instead of
offering Restore.
Follow-ups on the dismissed-detail guard:

- Don't redirect on a stale cache snapshot. After a dismissal the suppress
  mutation invalidates but doesn't rewrite reportKeys.detail, so the gate can
  briefly surface the pre-dismissal (e.g. ready) record via initialData. The
  redirect now waits for the detail query to settle (!isFetching) — which the
  query forces fresh on mount via initialDataUpdatedAt: 0 — so a just-dismissed
  report opened from the Dismissed list no longer bounces straight out.
- Route non-suppressed reports by the same membership predicates the tabs use:
  Pulls when a PR exists, Runs for in-flight runs (potential/candidate/
  in_progress/pending_input), else Reports — instead of sending every non-PR
  report to Reports, which left run-state reports on the wrong tab.
…direct

isReportTabReport excludes failed (failed runs live only in the Runs tab), so
the previous isAgentRunReport check sent a restored-then-failed report to
/reports, which doesn't list it. Route by membership precedence instead:
PR -> pulls, isReportTabReport -> reports, else -> runs.
#54: a suppressed report reached via a stale /pulls, /reports, or /runs URL
rendered that tab's full triage actions (dismiss, discuss, create PR) on an
out-of-pipeline report, now that the backend serves suppressed reports on
retrieve. Centralize the status<->route guard in InboxReportDetailGate: redirect
across the dismissed<->pipeline boundary in both directions (suppressed off the
triage routes -> dismissed; non-suppressed off /dismissed -> its current home),
gated on a settled fetch. This subsumes the bespoke guard that lived in
DismissedReportDetail, so that component is back to just rendering.

#59: harden useInboxRestoreReport to re-read the report and no-op (with a toast
+ list refresh) if it's no longer suppressed, covering the card-level Restore
on a row left stale by a change in another session — not just the detail path.
Aligns the inbox archive terminology with the PostHog Cloud "Archive" wording:

- "Dismissed" tab → "Archive"; tab/card/detail copy and empty state now say archived.
- "Dismiss" action → "Archive" (single-report button + dialog title/confirm, card and detail tooltips/aria-labels, bulk-action toast).
- The suppress-permanently option tooltip and the suppress success toast now read "archive(d)".

Internal identifiers keep the existing vocabulary on purpose: the route segment
(/code/inbox/dismissed), query keys, component/hook/file names, and the backend
status (suppressed) are unchanged, so analytics and deep links stay stable. The
distinct "Snooze" and "Suppress" bulk labels are left as-is.

Also addresses the gate review comment: on a triage route, InboxReportDetailGate
now holds the spinner while the forced post-mount fetch confirms the status,
so a report suppressed in another session can't briefly show triage actions
(create PR / discuss / archive) from a stale cached snapshot before the redirect
fires. The Archive route keeps its instant render-from-cache.

Generated-By: PostHog Code
Task-Id: c0aa88e1-2b85-49c1-8f0e-d6b98f20b251
Address Michael's review on #2704:

- Replace the standalone DismissedReportCard with an `archived` variant of
  ReportCard, so the archive list reuses the same card chrome (priority
  monogram, title, headline, source meta, findings count) instead of
  duplicating ~150 lines. The variant swaps triage affordances for a single
  Restore action, shows the archive date + dismissal reason in the metadata
  row, links to the read-only archived detail view, and skips the artefact
  fetch it doesn't render.
- Clarify why `suppressed` is the only status the Archive tab lists: there is
  no separate dismissed/resolved status in the enum, `deleted` is permanent,
  and snooze is a temporary timestamp that auto-returns — none belong in a
  manual restore list.

Generated-By: PostHog Code
Task-Id: c0aa88e1-2b85-49c1-8f0e-d6b98f20b251
@Twixes Twixes force-pushed the inbox/dismissed-tab branch from e5ae02c to 4f3f02b Compare June 18, 2026 14:09
@Twixes Twixes assigned Twixes and unassigned andrewm4894 Jun 18, 2026
@Twixes Twixes merged commit 1f6f030 into main Jun 18, 2026
23 checks passed
@Twixes Twixes deleted the inbox/dismissed-tab branch June 18, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants