From ea8bbd21ae92b5740799c980289a38062e38a1a0 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 6 May 2026 08:11:24 +0000 Subject: [PATCH 1/8] feat(inbox): suppress dialog with dismissal feedback Add a Suppress button to the top of the report detail pane and replace the bulk-suppress confirmation in the toolbar with a dialog that asks why the user is suppressing. The dialog offers six radio reasons plus an optional free-form note. Selections are sent alongside the state transition and persisted server-side as a `dismissal`-type report artefact so the rationale survives status changes and can stack over time. Generated-By: PostHog Code Task-Id: d3d675ab-707a-4a35-939b-9a9ab0f5c5b5 --- apps/code/src/renderer/api/posthogClient.ts | 49 +++++- .../inbox/components/SuppressDialog.tsx | 145 ++++++++++++++++++ .../components/detail/ReportDetailPane.tsx | 52 ++++++- .../inbox/components/list/SignalsToolbar.tsx | 54 ++----- .../inbox/hooks/useInboxBulkActions.ts | 54 +++++-- apps/code/src/shared/types.ts | 29 ++++ 6 files changed, 322 insertions(+), 61 deletions(-) create mode 100644 apps/code/src/renderer/features/inbox/components/SuppressDialog.tsx diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 1c1e10fb1..3201a4610 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -4,6 +4,8 @@ import type { ActionabilityJudgmentArtefact, AvailableSuggestedReviewer, AvailableSuggestedReviewersResponse, + DismissalArtefact, + DismissalReason, PriorityJudgmentArtefact, SandboxEnvironment, SandboxEnvironmentInput, @@ -262,7 +264,17 @@ type AnyArtefact = | PriorityJudgmentArtefact | ActionabilityJudgmentArtefact | SignalFindingArtefact - | SuggestedReviewersArtefact; + | SuggestedReviewersArtefact + | DismissalArtefact; + +const DISMISSAL_REASONS = new Set([ + "already_fixed", + "analysis_wrong", + "wontfix_intentional", + "wontfix_irrelevant", + "wrong_reviewer", + "other", +]); const PRIORITY_VALUES = new Set(["P0", "P1", "P2", "P3", "P4"]); @@ -367,6 +379,35 @@ function normalizeSignalFindingArtefact( }; } +function normalizeDismissalArtefact( + value: Record, +): DismissalArtefact | null { + const id = optionalString(value.id); + if (!id) return null; + + const contentValue = isObjectRecord(value.content) ? value.content : null; + if (!contentValue) return null; + + const rawReason = optionalString(contentValue.reason); + const reason = + rawReason && DISMISSAL_REASONS.has(rawReason as DismissalReason) + ? (rawReason as DismissalReason) + : null; + + return { + id, + type: "dismissal", + created_at: optionalString(value.created_at) ?? new Date(0).toISOString(), + content: { + reason, + note: optionalString(contentValue.note) ?? "", + user_id: + typeof contentValue.user_id === "number" ? contentValue.user_id : null, + user_uuid: optionalString(contentValue.user_uuid), + }, + }; +} + function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { if (!isObjectRecord(value)) { return null; @@ -382,6 +423,9 @@ function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { if (dispatchType === "priority_judgment") { return normalizePriorityJudgmentArtefact(value); } + if (dispatchType === "dismissal") { + return normalizeDismissalArtefact(value); + } const id = optionalString(value.id); if (!id) { @@ -1992,6 +2036,9 @@ export class PostHogAPIClient { snooze_for?: number; reset_weight?: boolean; error?: string; + /** Optional dismissal feedback persisted as a `dismissal` artefact. Only honored when state == "suppressed". */ + dismissal_reason?: DismissalReason; + dismissal_note?: string; }, ): Promise { const teamId = await this.getTeamId(); diff --git a/apps/code/src/renderer/features/inbox/components/SuppressDialog.tsx b/apps/code/src/renderer/features/inbox/components/SuppressDialog.tsx new file mode 100644 index 000000000..26c1ac812 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/SuppressDialog.tsx @@ -0,0 +1,145 @@ +import { Button } from "@components/ui/Button"; +import { EyeSlashIcon } from "@phosphor-icons/react"; +import { + Dialog, + Flex, + RadioGroup, + Spinner, + Text, + TextArea, +} from "@radix-ui/themes"; +import type { DismissalReason } from "@shared/types"; +import { useEffect, useState } from "react"; + +interface DismissalReasonOption { + value: DismissalReason; + label: string; +} + +const DISMISSAL_REASON_OPTIONS: readonly DismissalReasonOption[] = [ + { value: "already_fixed", label: "Already fixed elsewhere" }, + { value: "analysis_wrong", label: "Agent's analysis is wrong" }, + { value: "wontfix_intentional", label: "Won't fix - intentional behavior" }, + { + value: "wontfix_irrelevant", + label: "Won't fix - issue is real but irrelevant", + }, + { value: "wrong_reviewer", label: "I'm not the right reviewer" }, + { value: "other", label: "Other" }, +] as const; + +export interface SuppressDialogResult { + reason: DismissalReason; + note: string; +} + +export interface SuppressDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** Number of reports being suppressed; controls heading wording. */ + reportCount: number; + isSubmitting: boolean; + onConfirm: (result: SuppressDialogResult) => void; +} + +export function SuppressDialog({ + open, + onOpenChange, + reportCount, + isSubmitting, + onConfirm, +}: SuppressDialogProps) { + const [reason, setReason] = useState(null); + const [note, setNote] = useState(""); + + useEffect(() => { + if (open) { + setReason(null); + setNote(""); + } + }, [open]); + + const isPlural = reportCount !== 1; + const reportNoun = isPlural ? "reports" : "report"; + + const handleConfirm = () => { + if (!reason) return; + onConfirm({ reason, note: note.trim() }); + }; + + return ( + + + + + + + {isPlural ? `Suppress ${reportCount} reports` : "Suppress report"} + + + + + Why are you suppressing {isPlural ? "these" : "this"} {reportNoun}? + Your feedback is saved with the {reportNoun} and helps PostHog + improve future reports. + + + setReason(value as DismissalReason)} + > + + {DISMISSAL_REASON_OPTIONS.map((option) => ( + + + + {option.label} + + + ))} + + + + + + Optional: add detail + +