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
88 changes: 88 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type {
SandboxEnvironment,
SandboxEnvironmentInput,
SignalReport,
SignalReportArtefact,
SignalReportArtefactsResponse,
SignalReportSignalsResponse,
SignalReportStatus,
SignalReportsQueryParams,
SignalReportsResponse,
SuggestedReviewersArtefact,
Expand Down Expand Up @@ -1068,6 +1070,92 @@ export class PostHogAPIClient {
}
}

async updateSignalReportState(
reportId: string,
input: {
state: Extract<SignalReportStatus, "suppressed" | "potential">;
snooze_for?: number;
reset_weight?: boolean;
error?: string;
},
): Promise<SignalReport> {
const teamId = await this.getTeamId();
const url = new URL(
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/state/`,
);
const path = `/api/projects/${teamId}/signal_reports/${reportId}/state/`;

const response = await this.api.fetcher.fetch({
method: "post",
url,
path,
overrides: {
body: JSON.stringify(input),
},
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || "Failed to update signal report state");
}

return (await response.json()) as SignalReport;
}

async deleteSignalReport(reportId: string): Promise<{
status: "deletion_started" | "already_running";
report_id: string;
}> {
const teamId = await this.getTeamId();
const url = new URL(
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/`,
);
const path = `/api/projects/${teamId}/signal_reports/${reportId}/`;

const response = await this.api.fetcher.fetch({
method: "delete",
url,
path,
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || "Failed to delete signal report");
}

return (await response.json()) as {
status: "deletion_started" | "already_running";
report_id: string;
};
}

async reingestSignalReport(reportId: string): Promise<{
status: "reingestion_started" | "already_running";
report_id: string;
}> {
const teamId = await this.getTeamId();
const url = new URL(
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/reingest/`,
);
const path = `/api/projects/${teamId}/signal_reports/${reportId}/reingest/`;

const response = await this.api.fetcher.fetch({
method: "post",
url,
path,
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || "Failed to reingest signal report");
}

return (await response.json()) as {
status: "reingestion_started" | "already_running";
report_id: string;
};
}

async getMcpServers(): Promise<McpRecommendedServer[]> {
const teamId = await this.getTeamId();
const url = new URL(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AnimatedEllipsis } from "@features/inbox/components/utils/AnimatedEllipsis";
import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/SourceProductIcons";
import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons";
import { ArrowDownIcon } from "@phosphor-icons/react";
import { Box, Button, Flex, Text, Tooltip } from "@radix-ui/themes";
import explorerHog from "@renderer/assets/images/explorer-hog.png";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail";
import { InboxSourcesDialog } from "@features/inbox/components/InboxSourcesDialog";
import { useInboxReportsInfinite } from "@features/inbox/hooks/useInboxReports";
import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs";
import { useInboxReportSelectionStore } from "@features/inbox/stores/inboxReportSelectionStore";
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore";
import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore";
Expand Down Expand Up @@ -107,6 +108,13 @@ export function InboxSignalsTab() {

// ── Selection state ─────────────────────────────────────────────────────
const [selectedReportId, setSelectedReportId] = useState<string | null>(null);
const selectedReportIds = useInboxReportSelectionStore(
(s) => s.selectedReportIds ?? [],
);
const toggleReportSelection = useInboxReportSelectionStore(
(s) => s.toggleReportSelection,
);
const pruneSelection = useInboxReportSelectionStore((s) => s.pruneSelection);

useEffect(() => {
if (reports.length === 0) {
Expand All @@ -124,6 +132,10 @@ export function InboxSignalsTab() {
}
}, [reports, selectedReportId]);

useEffect(() => {
pruneSelection(reports.map((report) => report.id));
}, [reports, pruneSelection]);

const selectedReport = useMemo(
() => reports.find((report) => report.id === selectedReportId) ?? null,
[reports, selectedReportId],
Expand Down Expand Up @@ -201,19 +213,24 @@ export function InboxSignalsTab() {
selectedReportIdRef.current = selectedReportId;
const leftPaneRef = useRef<HTMLDivElement>(null);

const focusListPane = useCallback(() => {
requestAnimationFrame(() => {
leftPaneRef.current?.focus();
});
}, []);

// Auto-focus the list pane when the two-pane layout appears
useEffect(() => {
if (showTwoPaneLayout) {
// Small delay to ensure the ref is mounted after conditional render
requestAnimationFrame(() => {
leftPaneRef.current?.focus();
});
focusListPane();
}
}, [showTwoPaneLayout]);
}, [focusListPane, showTwoPaneLayout]);

const navigateReport = useCallback((direction: 1 | -1) => {
const list = reportsRef.current;
if (list.length === 0) return;

const currentId = selectedReportIdRef.current;
const currentIndex = currentId
? list.findIndex((r) => r.id === currentId)
Expand All @@ -223,10 +240,22 @@ export function InboxSignalsTab() {
? 0
: Math.max(0, Math.min(list.length - 1, currentIndex + direction));
const nextId = list[nextIndex].id;

setSelectedReportId(nextId);
leftPaneRef.current
?.querySelector(`[data-report-id="${nextId}"]`)
?.scrollIntoView({ block: "nearest" });

const container = leftPaneRef.current;
const row = container?.querySelector<HTMLElement>(
`[data-report-id="${nextId}"]`,
);
const stickyHeader = container?.querySelector<HTMLElement>(
"[data-inbox-sticky-header]",
);

if (!row) return;

const stickyHeaderHeight = stickyHeader?.offsetHeight ?? 0;
row.style.scrollMarginTop = `${stickyHeaderHeight}px`;
row.scrollIntoView({ block: "nearest" });
}, []);

// Window-level keyboard handler so arrow keys work regardless of which
Expand All @@ -243,18 +272,22 @@ export function InboxSignalsTab() {

const target = e.target as HTMLElement;
if (target.closest("input, select, textarea")) return;
if (e.key === " " && target.closest("button, [role='checkbox']")) return;

if (e.key === "ArrowDown") {
e.preventDefault();
navigateReport(1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
navigateReport(-1);
} else if (e.key === " " && selectedReportIdRef.current) {
e.preventDefault();
toggleReportSelection(selectedReportIdRef.current);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [navigateReport]);
}, [navigateReport, toggleReportSelection]);

const searchDisabledReason =
!hasReports && !searchQuery.trim()
Expand Down Expand Up @@ -287,11 +320,33 @@ export function InboxSignalsTab() {
<Flex
ref={leftPaneRef}
direction="column"
tabIndex={-1}
tabIndex={0}
className="outline-none"
onMouseDownCapture={(e) => {
const target = e.target as HTMLElement;
if (
target.closest(
"[data-report-id], button, input, select, textarea, [role='checkbox']",
)
) {
focusListPane();
}
}}
onFocusCapture={(e) => {
const target = e.target as HTMLElement;
if (
target !== leftPaneRef.current &&
target.closest(
"[data-report-id], button, input, select, textarea, [role='checkbox']",
)
) {
focusListPane();
}
}}
>
<InboxLiveRail active={inboxPollingActive} />
<Box
data-inbox-sticky-header
style={{
position: "sticky",
top: 0,
Expand All @@ -306,6 +361,7 @@ export function InboxSignalsTab() {
livePolling={inboxPollingActive}
readyCount={readyCount}
processingCount={processingCount}
reports={reports}
/>
</Box>
<ReportListPane
Expand All @@ -322,7 +378,9 @@ export function InboxSignalsTab() {
searchQuery={searchQuery}
hasActiveFilters={hasActiveFilters}
selectedReportId={selectedReportId}
selectedReportIds={selectedReportIds}
onSelectReport={setSelectedReportId}
onToggleReportSelection={toggleReportSelection}
/>
</Flex>
</ScrollArea>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MarkdownRenderer } from "@features/editor/components/MarkdownRenderer";
import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/SourceProductIcons";
import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons";
import {
ArrowSquareOutIcon,
CaretDownIcon,
Expand Down
Loading
Loading