Skip to content
Open
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
70 changes: 62 additions & 8 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,7 +18,6 @@ import type {
SignalReportArtefact,
SignalReportArtefactsResponse,
SignalReportSignalsResponse,
SignalReportStatus,
SignalReportsQueryParams,
SignalReportsResponse,
SignalReportTask,
Expand Down Expand Up @@ -262,7 +266,12 @@ type AnyArtefact =
| PriorityJudgmentArtefact
| ActionabilityJudgmentArtefact
| SignalFindingArtefact
| SuggestedReviewersArtefact;
| SuggestedReviewersArtefact
| DismissalArtefact;

const DISMISSAL_REASONS = new Set<DismissalReasonOptionValue>(
DISMISSAL_REASON_OPTIONS.map((o) => o.value),
);

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

Expand Down Expand Up @@ -367,6 +376,39 @@ 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 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;
Expand All @@ -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) {
Expand Down Expand Up @@ -1987,12 +2032,21 @@ export class PostHogAPIClient {

async updateSignalReportState(
reportId: string,
input: {
state: Extract<SignalReportStatus, "suppressed" | "potential">;
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<SignalReport> {
const teamId = await this.getTeamId();
const url = new URL(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DismissalReasonOptionValue | null>(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 (
<AlertDialog.Root open={open} onOpenChange={onOpenChange}>
<AlertDialog.Content maxWidth="480px">
<AlertDialog.Title>
<Text className="text-balance font-bold leading-tight">
{titleText}
</Text>
</AlertDialog.Title>
<AlertDialog.Description className="text-sm">
<Text color="gray" className="text-[13px]">
This report will be removed from your inbox. Your feedback is saved
on the report and helps the agents do better.
</Text>
</AlertDialog.Description>

<Flex direction="column" gap="4" mt="2">
<RadioGroup.Root
size="1"
value={reason ?? ""}
onValueChange={(value) =>
setReason(value as DismissalReasonOptionValue)
}
>
<Flex direction="column" gap="2">
{DISMISSAL_REASON_OPTIONS.map((option) => {
const snoozesInsteadOfDismiss = isDismissalReasonSnooze(
option.value,
);
const disabled =
snoozesInsteadOfDismiss && alreadyFixedDisabled;

return snoozesInsteadOfDismiss ? (
<ExplainedPauseLabel
key={option.value}
label={option.label}
value={option.value}
disabled={disabled}
disabledReason={disabled ? snoozeDisabledReason : undefined}
/>
) : (
<ExplainedSuppressLabel
key={option.value}
label={option.label}
value={option.value}
/>
);
})}
</Flex>
</RadioGroup.Root>

<TextArea
value={note}
onChange={(event) => setNote(event.target.value)}
placeholder="Optional: add detail"
size="1"
rows={3}
maxLength={4000}
disabled={isSubmitting}
/>
</Flex>

<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<Button
variant="solid"
color="gray"
disabled={!reason || isSubmitting}
disabledReason={!reason ? "you haven't picked a reason" : null}
onClick={handleConfirm}
loading={isSubmitting}
>
Dismiss & teach the agent
</Button>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
WelcomePane,
} from "@features/inbox/components/InboxEmptyStates";
import { InboxSourcesDialog } from "@features/inbox/components/InboxSourcesDialog";
import {
inboxBulkSnoozeDisabledReason,
inboxBulkSuppressDisabledReason,
useInboxBulkActions,
} from "@features/inbox/hooks/useInboxBulkActions";
import { useInboxDeepLinkListSync } from "@features/inbox/hooks/useInboxDeepLinkListSync";
import {
useInboxAvailableSuggestedReviewers,
Expand Down Expand Up @@ -32,10 +37,15 @@ import {
useRepositoryIntegration,
} from "@hooks/useIntegrations";
import { Box, Flex, ScrollArea } from "@radix-ui/themes";
import type { SignalReportsQueryParams } from "@shared/types";
import { isDismissalReasonSnooze } from "@shared/dismissalReasons";
import type { SignalReport, SignalReportsQueryParams } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
DismissReportDialog,
type DismissReportDialogResult,
} from "./DismissReportDialog";
import { MultiSelectStack } from "./detail/MultiSelectStack";
import { ReportDetailPane } from "./detail/ReportDetailPane";
import { GitHubConnectionBanner } from "./list/GitHubConnectionBanner";
Expand Down Expand Up @@ -209,11 +219,44 @@ export function InboxSignalsTab() {
);
const clearSelection = useInboxReportSelectionStore((s) => s.clearSelection);

const [dismissReport, setDismissReport] = useState<SignalReport | null>(null);

const dismissTargetId = dismissReport?.id ?? null;
const dismissBulkActions = useInboxBulkActions(allReports, dismissTargetId);

const handleDismissDialogOpenChange = useCallback((open: boolean) => {
if (!open) setDismissReport(null);
}, []);

const handleDismissConfirm = useCallback(
async (result: DismissReportDialogResult) => {
if (dismissTargetId == null) return;
const ok = isDismissalReasonSnooze(result.reason)
? await dismissBulkActions.snoozeSelected()
: await dismissBulkActions.suppressSelected(result);
if (ok) {
setDismissReport(null);
}
},
[dismissBulkActions, dismissTargetId],
);

const { selectedReport } = useInboxDeepLinkListSync({
reports,
inboxPollingActive,
});

const openDismissDialogFromToolbar = useCallback(() => {
if (selectedReportIds.length !== 1) return;
const id = selectedReportIds[0];
const report = allReports.find((r) => r.id === id);
if (report) setDismissReport(report);
}, [selectedReportIds, allReports]);

const dismissMutationPending =
dismissReport != null &&
(dismissBulkActions.isSuppressing || dismissBulkActions.isSnoozing);

// Stable refs so callbacks don't need re-registration on every render
const selectedReportIdsRef = useRef(selectedReportIds);
selectedReportIdsRef.current = selectedReportIds;
Expand Down Expand Up @@ -549,6 +592,8 @@ export function InboxSignalsTab() {
effectiveBulkIds={selectedReportIds}
onToggleSelectAll={handleToggleSelectAll}
onConfigureSources={() => setSourcesDialogOpen(true)}
onOpenDismissDialog={openDismissDialogFromToolbar}
isDismissMutationPending={dismissMutationPending}
/>
</Box>
<RecommendedSetupTasks
Expand Down Expand Up @@ -600,6 +645,12 @@ export function InboxSignalsTab() {
<ReportDetailPane
report={selectedReport}
onClose={clearSelection}
onRequestDismissReport={() => setDismissReport(selectedReport)}
suppressDisabledReason={inboxBulkSuppressDisabledReason(
allReports,
[selectedReport.id],
)}
isDismissMutationPending={dismissMutationPending}
/>
) : selectedDiscoveredTask ? (
<DiscoveredTaskDetailPane
Expand Down Expand Up @@ -654,6 +705,22 @@ export function InboxSignalsTab() {
hasSignalSources={hasSignalSources}
hasGithubIntegration={hasGithubIntegration}
/>

{dismissReport != null ? (
<DismissReportDialog
key={dismissReport.id}
open
onOpenChange={handleDismissDialogOpenChange}
report={dismissReport}
isSubmitting={
dismissBulkActions.isSuppressing || dismissBulkActions.isSnoozing
}
snoozeDisabledReason={inboxBulkSnoozeDisabledReason(allReports, [
dismissReport.id,
])}
onConfirm={(result) => void handleDismissConfirm(result)}
/>
) : null}
</>
);
}
Loading
Loading