Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
ActionabilityJudgmentArtefact,
AvailableSuggestedReviewer,
AvailableSuggestedReviewersResponse,
DismissalArtefact,
DismissalReason,
PriorityJudgmentArtefact,
SandboxEnvironment,
SandboxEnvironmentInput,
Expand Down Expand Up @@ -262,7 +264,17 @@ type AnyArtefact =
| PriorityJudgmentArtefact
| ActionabilityJudgmentArtefact
| SignalFindingArtefact
| SuggestedReviewersArtefact;
| SuggestedReviewersArtefact
| DismissalArtefact;

const DISMISSAL_REASONS = new Set<DismissalReason>([
"already_fixed",
"analysis_wrong",
"wontfix_intentional",
"wontfix_irrelevant",
"wrong_reviewer",
"other",
]);

const PRIORITY_VALUES = new Set(["P0", "P1", "P2", "P3", "P4"]);

Expand Down Expand Up @@ -367,6 +379,35 @@ function normalizeSignalFindingArtefact(
};
}

function normalizeDismissalArtefact(
value: Record<string, unknown>,
): 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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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<SignalReport> {
const teamId = await this.getTeamId();
Expand Down
145 changes: 145 additions & 0 deletions apps/code/src/renderer/features/inbox/components/SuppressDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<DismissalReason | null>(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 (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content maxWidth="480px" size="1">
<Flex direction="column" gap="3">
<Flex align="center" gap="2">
<EyeSlashIcon size={16} />
<Text className="font-medium text-sm">
{isPlural ? `Suppress ${reportCount} reports` : "Suppress report"}
</Text>
</Flex>

<Text color="gray" className="text-[13px]">
Why are you suppressing {isPlural ? "these" : "this"} {reportNoun}?
Your feedback is saved with the {reportNoun} and helps PostHog
improve future reports.
</Text>

<RadioGroup.Root
size="1"
value={reason ?? ""}
onValueChange={(value) => setReason(value as DismissalReason)}
>
<Flex direction="column" gap="2">
{DISMISSAL_REASON_OPTIONS.map((option) => (
<Text
key={option.value}
as="label"
className="cursor-pointer text-[13px]"
>
<Flex align="center" gap="2">
<RadioGroup.Item value={option.value} />
{option.label}
</Flex>
</Text>
))}
</Flex>
</RadioGroup.Root>

<Flex direction="column" gap="1">
<Text color="gray" className="text-[13px]">
Optional: add detail
</Text>
<TextArea
value={note}
onChange={(event) => setNote(event.target.value)}
placeholder="Anything else worth mentioning?"
size="1"
rows={3}
maxLength={4000}
disabled={isSubmitting}
/>
</Flex>

<Flex gap="2" justify="end">
<Dialog.Close>
<Button size="1" variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Button
size="1"
variant="solid"
color="orange"
disabled={!reason || isSubmitting}
disabledReason={!reason ? "you haven't picked a reason" : null}
onClick={handleConfirm}
>
{isSubmitting ? <Spinner size="1" /> : <EyeSlashIcon size={12} />}
Suppress
</Button>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Badge } from "@components/ui/Badge";
import { Button } from "@components/ui/Button";
import { useInboxBulkActions } from "@features/inbox/hooks/useInboxBulkActions";
import {
useInboxReportArtefacts,
useInboxReportSignals,
Expand All @@ -11,17 +13,26 @@ import {
CaretDownIcon,
CaretRightIcon,
EyeIcon,
EyeSlashIcon,
LinkSimpleIcon,
WarningIcon,
XIcon,
} from "@phosphor-icons/react";
import { Box, Flex, ScrollArea, Text, Tooltip } from "@radix-ui/themes";
import {
Box,
Flex,
ScrollArea,
Spinner,
Text,
Tooltip,
} from "@radix-ui/themes";
import { useTRPC } from "@renderer/trpc";
import { EXTERNAL_LINKS } from "@renderer/utils/links";
import { getDeeplinkProtocol } from "@shared/deeplink";
import type {
ActionabilityJudgmentArtefact,
ActionabilityJudgmentContent,
DismissalReason,
PriorityJudgmentArtefact,
SignalFindingArtefact,
SignalReport,
Expand All @@ -34,6 +45,7 @@ import { useNavigationStore } from "@stores/navigationStore";
import { useQuery } from "@tanstack/react-query";
import { type ReactNode, useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { SuppressDialog } from "../SuppressDialog";
import { SignalReportActionabilityBadge } from "../utils/SignalReportActionabilityBadge";
import { SignalReportPriorityBadge } from "../utils/SignalReportPriorityBadge";
import { SignalReportStatusBadge } from "../utils/SignalReportStatusBadge";
Expand Down Expand Up @@ -229,6 +241,21 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
report.actionability === "immediately_actionable" &&
report.already_addressed !== true);

// ── Suppress action ─────────────────────────────────────────────────────
const [suppressDialogOpen, setSuppressDialogOpen] = useState(false);
const { suppressDisabledReason, isSuppressing, suppressSelected } =
useInboxBulkActions([report], [report.id]);
const handleConfirmSuppress = useCallback(
async (result: { reason: DismissalReason; note: string }) => {
const ok = await suppressSelected(result);
if (ok) {
setSuppressDialogOpen(false);
onClose();
}
},
[suppressSelected, onClose],
);

const handleCreateImplementationTask = useCallback(() => {
if (!canCreateImplementationPr) return;
navigateToTaskInput({
Expand Down Expand Up @@ -264,7 +291,20 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
{report.title ?? "Untitled signal"}
</Text>
</Flex>
<Flex align="center" gap="1" className="shrink-0">
<Flex align="center" gap="2" className="shrink-0">
<Button
size="1"
variant="soft"
color="red"
className="text-[12px]"
tooltipContent="Suppress this report and tell PostHog why"
disabledReason={suppressDisabledReason}
disabled={suppressDisabledReason !== null || isSuppressing}
onClick={() => setSuppressDialogOpen(true)}
>
{isSuppressing ? <Spinner size="1" /> : <EyeSlashIcon size={12} />}
Suppress
</Button>
<Tooltip content="Copy link to this report">
<button
type="button"
Expand Down Expand Up @@ -541,6 +581,14 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
canCreateImplementationPr ? handleCreateImplementationTask : undefined
}
/>

<SuppressDialog
open={suppressDialogOpen}
onOpenChange={setSuppressDialogOpen}
reportCount={1}
isSubmitting={isSuppressing}
onConfirm={(result) => void handleConfirmSuppress(result)}
/>
</>
);
}
Loading
Loading