From 7b8494e78f2c6881d117974c66df58db001727eb Mon Sep 17 00:00:00 2001 From: Alex Lebedev Date: Thu, 27 Aug 2026 12:48:17 +0200 Subject: [PATCH 01/24] feat(signals): wrong-repo dismissals feed repo selection Adds a first-class wrong_repo dismissal reason with an optional "which repository should it have been?" picker in the desktop and web inbox dismiss dialogs. The correction is recorded on the dismissal artefact (with the repo the pipeline had selected), becomes the report's latest repo_selection artefact when connected, and recent corrections are rendered into the repo selection agent's prompt on every signals-side selection so the same mistake is not repeated. Generated-By: PostHog Desktop Task-Id: 70848b5e-fd66-4cf9-a68e-e1551e205f38 --- .../components/Feed/FeedTabContent.tsx | 2 +- .../frontend/components/Feed/feedLogic.ts | 23 +-- .../packages/api-client/src/posthog-client.ts | 2 + .../packages/core/src/inbox/bulkActions.ts | 6 + .../packages/core/src/inbox/engagement.ts | 9 +- .../packages/shared/src/analytics-events.ts | 2 + .../packages/shared/src/dismissal-reasons.ts | 4 + .../inbox/components/DismissReportDialog.tsx | 57 ++++++- .../inbox/hooks/useInboxBulkActions.ts | 20 +-- products/signals/ARCHITECTURE.md | 36 +++-- products/signals/backend/artefact_schemas.py | 15 ++ products/signals/backend/receivers.py | 1 + products/signals/backend/repo_corrections.py | 146 ++++++++++++++++++ .../backend/report_generation/select_repo.py | 6 + .../backend/test/test_repo_corrections.py | 100 ++++++++++++ .../backend/test/test_signal_report_api.py | 75 +++++++++ products/signals/backend/views.py | 130 ++++++++++++++-- .../signals/frontend/generated/api.schemas.ts | 18 ++- products/signals/frontend/generated/api.ts | 9 +- .../signals/frontend/generated/api.zod.ts | 37 ++++- .../inbox/components/InboxReportList.tsx | 6 +- .../inbox/components/cards/ReportCard.tsx | 4 +- .../components/cards/useReportArchive.ts | 16 +- .../shell/DismissCorrectedRepoField.tsx | 50 ++++++ .../components/shell/DismissReportDialog.tsx | 37 +++-- .../shell/InboxBulkSelectionBar.tsx | 2 +- .../inbox/logics/inboxBulkActionsLogic.ts | 36 ++--- .../frontend/inbox/logics/inboxTriageLogic.ts | 30 ++-- .../frontend/inbox/logics/reportListLogic.ts | 23 +-- products/signals/frontend/inbox/types.ts | 2 + .../frontend/inbox/utils/dismissalReasons.ts | 12 ++ .../backend/logic/repo_selection/agent.py | 33 +++- .../tests/test_repo_selection_prompt.py | 16 ++ services/mcp/src/api/generated.ts | 18 ++- services/mcp/src/generated/signals/api.ts | 37 ++++- services/mcp/src/tools/generated/signals.ts | 6 + 36 files changed, 875 insertions(+), 151 deletions(-) create mode 100644 products/signals/backend/repo_corrections.py create mode 100644 products/signals/backend/test/test_repo_corrections.py create mode 100644 products/signals/frontend/inbox/components/shell/DismissCorrectedRepoField.tsx create mode 100644 products/tasks/backend/tests/test_repo_selection_prompt.py diff --git a/products/customer_analytics/frontend/components/Feed/FeedTabContent.tsx b/products/customer_analytics/frontend/components/Feed/FeedTabContent.tsx index 69e55010be49..5a7b27fdd121 100644 --- a/products/customer_analytics/frontend/components/Feed/FeedTabContent.tsx +++ b/products/customer_analytics/frontend/components/Feed/FeedTabContent.tsx @@ -41,7 +41,7 @@ export function FeedTabContent(): JSX.Element { key={report.id} report={report} backUrl={urls.customerAnalyticsFeed()} - onArchive={(reason, note) => archiveReport(report.id, reason, note)} + onArchive={(dismissal) => archiveReport(report.id, dismissal)} /> ))} diff --git a/products/customer_analytics/frontend/components/Feed/feedLogic.ts b/products/customer_analytics/frontend/components/Feed/feedLogic.ts index 2f771506e243..2a3f86a694eb 100644 --- a/products/customer_analytics/frontend/components/Feed/feedLogic.ts +++ b/products/customer_analytics/frontend/components/Feed/feedLogic.ts @@ -14,7 +14,7 @@ import { InboxSortField, } from 'products/signals/frontend/inbox/logics/inboxFiltersLogic' import { SignalReport, SignalReportPriority, SignalReportStatus } from 'products/signals/frontend/inbox/types' -import { DismissalReasonValue } from 'products/signals/frontend/inbox/utils/dismissalReasons' +import { DismissalFeedback } from 'products/signals/frontend/inbox/utils/dismissalReasons' import type { UserType } from '../../../../../frontend/src/types' @@ -52,17 +52,9 @@ export interface feedLogicValues { export interface feedLogicActions { archiveReport: ( reportId: string, - reason: DismissalReasonValue, - note: string + dismissal: DismissalFeedback ) => { - note: string - reason: - | 'already_fixed' - | 'analysis_wrong' - | 'other' - | 'report_unclear' - | 'wontfix_intentional' - | 'wontfix_irrelevant' + dismissal: DismissalFeedback reportId: string } clearFilters: () => { @@ -156,7 +148,7 @@ export const feedLogic = kea([ toggleScout: (scout: string) => ({ scout }), clearScoutFilter: true, clearFilters: true, - archiveReport: (reportId: string, reason: DismissalReasonValue, note: string) => ({ reportId, reason, note }), + archiveReport: (reportId: string, dismissal: DismissalFeedback) => ({ reportId, dismissal }), }), loaders(({ values }) => ({ reportsResponse: [ @@ -272,12 +264,13 @@ export const feedLogic = kea([ await breakpoint(300) actions.loadReports(null) }, - archiveReport: async ({ reportId, reason, note }) => { + archiveReport: async ({ reportId, dismissal }) => { try { await api.signalReports.setState(reportId, { state: 'suppressed', - dismissal_reason: reason, - ...(note ? { dismissal_note: note } : {}), + dismissal_reason: dismissal.reason, + ...(dismissal.note ? { dismissal_note: dismissal.note } : {}), + ...(dismissal.correctedRepository ? { corrected_repository: dismissal.correctedRepository } : {}), }) } catch (error: any) { lemonToast.error(error?.detail || error?.message || 'Failed to archive report') diff --git a/products/desktop/packages/api-client/src/posthog-client.ts b/products/desktop/packages/api-client/src/posthog-client.ts index 24b950452a13..9711c4e1a535 100644 --- a/products/desktop/packages/api-client/src/posthog-client.ts +++ b/products/desktop/packages/api-client/src/posthog-client.ts @@ -4980,6 +4980,8 @@ export class PostHogAPIClient { /** When omitted, the server suppresses without creating a dismissal artefact. */ dismissal_reason?: DismissalReasonOptionValue; dismissal_note?: string; + /** 'owner/repo' the report should have targeted; only allowed with dismissal_reason 'wrong_repo'. */ + corrected_repository?: string; reset_weight?: boolean; error?: string; }, diff --git a/products/desktop/packages/core/src/inbox/bulkActions.ts b/products/desktop/packages/core/src/inbox/bulkActions.ts index 0839bbd9c548..676707f50f4b 100644 --- a/products/desktop/packages/core/src/inbox/bulkActions.ts +++ b/products/desktop/packages/core/src/inbox/bulkActions.ts @@ -131,12 +131,15 @@ export function bulkSelectionKey(selection: InboxBulkSelection): string { export interface DismissReportInput { reason: DismissalReasonOptionValue; note: string; + /** 'owner/repo' the reports should have targeted; only set when reason is 'wrong_repo'. */ + correctedRepository?: string | null; } export type SuppressStateRequest = { state: "suppressed"; dismissal_reason?: DismissalReasonOptionValue; dismissal_note?: string; + corrected_repository?: string; }; /** Body for `updateSignalReportState` when suppressing/dismissing. Notes are clamped to 4000 chars. */ @@ -150,6 +153,9 @@ export function buildSuppressRequest( state: "suppressed", dismissal_reason: dismissal.reason, dismissal_note: dismissal.note.slice(0, 4000), + ...(dismissal.correctedRepository + ? { corrected_repository: dismissal.correctedRepository } + : {}), }; } diff --git a/products/desktop/packages/core/src/inbox/engagement.ts b/products/desktop/packages/core/src/inbox/engagement.ts index c23b86841391..72b184b74bda 100644 --- a/products/desktop/packages/core/src/inbox/engagement.ts +++ b/products/desktop/packages/core/src/inbox/engagement.ts @@ -132,7 +132,11 @@ export interface BuildBulkActionEventsInput { actionType: InboxBulkActionType; surface: InboxReportActionSurface; /** Dismissal metadata, only meaningful for `dismiss`. Note is truncated to 500 chars. */ - dismissal?: { reason?: string; note?: string }; + dismissal?: { + reason?: string; + note?: string; + correctedRepository?: string | null; + }; } /** @@ -168,6 +172,9 @@ export function buildBulkActionEvents( ...(actionType === "dismiss" && dismissal?.note ? { dismissal_note: dismissal.note.slice(0, 500) } : {}), + ...(actionType === "dismiss" && dismissal?.correctedRepository + ? { dismissal_corrected_repository: dismissal.correctedRepository } + : {}), })); } diff --git a/products/desktop/packages/shared/src/analytics-events.ts b/products/desktop/packages/shared/src/analytics-events.ts index 84c1732067f8..e6ddc6932819 100644 --- a/products/desktop/packages/shared/src/analytics-events.ts +++ b/products/desktop/packages/shared/src/analytics-events.ts @@ -792,6 +792,8 @@ export interface InboxReportActionProperties { list_size: number; dismissal_reason?: string; dismissal_note?: string; + // 'owner/repo' correction from a wrong_repo dismissal. + dismissal_corrected_repository?: string; signal_id?: string; signal_source_product?: string; signal_source_type?: string; diff --git a/products/desktop/packages/shared/src/dismissal-reasons.ts b/products/desktop/packages/shared/src/dismissal-reasons.ts index f0c9c4f05e02..ebd9b34d7a1a 100644 --- a/products/desktop/packages/shared/src/dismissal-reasons.ts +++ b/products/desktop/packages/shared/src/dismissal-reasons.ts @@ -16,6 +16,10 @@ export const DISMISSAL_REASON_OPTIONS = [ value: "analysis_wrong", label: "Agent's analysis is wrong", }, + { + value: "wrong_repo", + label: "Agent picked the wrong repository", + }, { value: "wontfix_intentional", label: "Won't fix - intentional behavior", diff --git a/products/desktop/packages/ui/src/features/inbox/components/DismissReportDialog.tsx b/products/desktop/packages/ui/src/features/inbox/components/DismissReportDialog.tsx index 02fabc733ac1..ed96a80b3733 100644 --- a/products/desktop/packages/ui/src/features/inbox/components/DismissReportDialog.tsx +++ b/products/desktop/packages/ui/src/features/inbox/components/DismissReportDialog.tsx @@ -4,10 +4,16 @@ import { isDismissalReasonSnooze, } from "@posthog/shared/dismissalReasons"; import type { SignalReport } from "@posthog/shared/types"; +import { GitHubRepoPicker } from "@posthog/ui/features/folder-picker/GitHubRepoPicker"; import { ExplainedPauseLabel, ExplainedSuppressLabel, } from "@posthog/ui/features/inbox/components/utils/ExplainedDismissOptionLabels"; +import { useIntegrationSelectors } from "@posthog/ui/features/integrations/store"; +import { + useGithubRepositories, + useIntegrations, +} from "@posthog/ui/features/integrations/useIntegrations"; import { Button } from "@posthog/ui/primitives/Button"; import { Dialog, Flex, RadioGroup, Text, TextArea } from "@radix-ui/themes"; import { useEffect, useRef, useState } from "react"; @@ -15,6 +21,8 @@ import { useEffect, useRef, useState } from "react"; export interface DismissReportDialogResult { reason: DismissalReasonOptionValue; note: string; + /** 'owner/repo' the reports should have targeted; only set when reason is 'wrong_repo'. */ + correctedRepository: string | null; } export interface DismissReportDialogProps { @@ -103,10 +111,29 @@ function DismissReportDialogBody({ }) { const [reason, setReason] = useState(null); const [note, setNote] = useState(""); + const [correctedRepository, setCorrectedRepository] = useState( + null, + ); + const [isRepoPickerOpen, setIsRepoPickerOpen] = useState(false); + const [repoSearch, setRepoSearch] = useState(""); + + const isWrongRepo = reason === "wrong_repo"; + // Populates the integration store in case no other surface loaded it yet; react-query dedupes. + useIntegrations(); + const { hasGithubIntegration } = useIntegrationSelectors(); + const repoPage = useGithubRepositories( + repoSearch, + isWrongRepo && isRepoPickerOpen, + ); const handleConfirm = () => { if (!reason) return; - onConfirm({ reason, note: note.trim() }); + onConfirm({ + reason, + note: note.trim(), + // A correction picked and then abandoned for another reason must not ride along. + correctedRepository: isWrongRepo ? correctedRepository : null, + }); }; const alreadyFixedDisabled = snoozeDisabledReason !== null; @@ -160,6 +187,34 @@ function DismissReportDialogBody({ + {isWrongRepo && hasGithubIntegration ? ( +
+ + Which repository should it have been? + + + + Optional. The agent uses your correction when picking repositories + in the future. + +
+ ) : null} +