diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 1c1e10fb1..5f739666e 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -1,9 +1,14 @@ import { isSupportedReasoningEffort } from "@posthog/agent/adapters/reasoning-effort"; import type { PermissionMode } from "@posthog/agent/execution-mode"; +import { + DISMISSAL_REASON_OPTIONS, + type DismissalReasonOptionValue, +} from "@shared/dismissalReasons"; import type { ActionabilityJudgmentArtefact, AvailableSuggestedReviewer, AvailableSuggestedReviewersResponse, + DismissalArtefact, PriorityJudgmentArtefact, SandboxEnvironment, SandboxEnvironmentInput, @@ -13,7 +18,6 @@ import type { SignalReportArtefact, SignalReportArtefactsResponse, SignalReportSignalsResponse, - SignalReportStatus, SignalReportsQueryParams, SignalReportsResponse, SignalReportTask, @@ -262,7 +266,12 @@ type AnyArtefact = | PriorityJudgmentArtefact | ActionabilityJudgmentArtefact | SignalFindingArtefact - | SuggestedReviewersArtefact; + | SuggestedReviewersArtefact + | DismissalArtefact; + +const DISMISSAL_REASONS = new Set( + DISMISSAL_REASON_OPTIONS.map((o) => o.value), +); const PRIORITY_VALUES = new Set(["P0", "P1", "P2", "P3", "P4"]); @@ -367,6 +376,39 @@ 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 DismissalReasonOptionValue) + ? (rawReason as DismissalReasonOptionValue) + : null; + + if (reason == null) { + return 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 +424,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) { @@ -1987,12 +2032,21 @@ export class PostHogAPIClient { async updateSignalReportState( reportId: string, - input: { - state: Extract; - snooze_for?: number; - reset_weight?: boolean; - error?: string; - }, + input: + | { + state: "potential"; + snooze_for?: number; + reset_weight?: boolean; + error?: string; + } + | { + state: "suppressed"; + /** When omitted, the server suppresses without creating a dismissal artefact. */ + dismissal_reason?: DismissalReasonOptionValue; + dismissal_note?: string; + reset_weight?: boolean; + error?: string; + }, ): Promise { const teamId = await this.getTeamId(); const url = new URL( diff --git a/apps/code/src/renderer/features/inbox/components/DismissReportDialog.tsx b/apps/code/src/renderer/features/inbox/components/DismissReportDialog.tsx new file mode 100644 index 000000000..5603a9db7 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/DismissReportDialog.tsx @@ -0,0 +1,147 @@ +import { Button } from "@components/ui/Button"; +import { + ExplainedPauseLabel, + ExplainedSuppressLabel, +} from "@features/inbox/components/utils/ExplainedDismissOptionLabels"; +import { + AlertDialog, + Flex, + RadioGroup, + Text, + TextArea, +} from "@radix-ui/themes"; +import { + DISMISSAL_REASON_OPTIONS, + type DismissalReasonOptionValue, + isDismissalReasonSnooze, +} from "@shared/dismissalReasons"; +import type { SignalReport } from "@shared/types"; +import { useEffect, useState } from "react"; + +export interface DismissReportDialogResult { + reason: DismissalReasonOptionValue; + note: string; +} + +export interface DismissReportDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + report: SignalReport; + isSubmitting: boolean; + /** + * When snooze is not allowed for the current selection, the "Already fixed elsewhere" + * option is disabled because that path snoozes instead of dismissing. + */ + snoozeDisabledReason: string | null; + onConfirm: (result: DismissReportDialogResult) => void; +} + +export function DismissReportDialog({ + open, + onOpenChange, + report, + isSubmitting, + snoozeDisabledReason, + onConfirm, +}: DismissReportDialogProps) { + const [reason, setReason] = useState(null); + const [note, setNote] = useState(""); + + useEffect(() => { + if (open) { + setReason(null); + setNote(""); + } + }, [open]); + + const handleConfirm = () => { + if (!reason) return; + onConfirm({ reason, note: note.trim() }); + }; + + const alreadyFixedDisabled = snoozeDisabledReason !== null; + + const titleText = `Dismiss report "${report.title?.trim() ? report.title : "Untitled signal"}"?`; + + return ( + + + + + {titleText} + + + + + This report will be removed from your inbox. Your feedback is saved + on the report and helps the agents do better. + + + + + + setReason(value as DismissalReasonOptionValue) + } + > + + {DISMISSAL_REASON_OPTIONS.map((option) => { + const snoozesInsteadOfDismiss = isDismissalReasonSnooze( + option.value, + ); + const disabled = + snoozesInsteadOfDismiss && alreadyFixedDisabled; + + return snoozesInsteadOfDismiss ? ( + + ) : ( + + ); + })} + + + +