Skip to content

Commit f32b156

Browse files
committed
sig: add modifying report state via inbox
1 parent fc531e9 commit f32b156

9 files changed

Lines changed: 1109 additions & 227 deletions

File tree

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type {
22
SandboxEnvironment,
33
SandboxEnvironmentInput,
4+
SignalReport,
45
SignalReportArtefact,
56
SignalReportArtefactsResponse,
67
SignalReportSignalsResponse,
8+
SignalReportStatus,
79
SignalReportsQueryParams,
810
SignalReportsResponse,
911
SuggestedReviewersArtefact,
@@ -1068,6 +1070,92 @@ export class PostHogAPIClient {
10681070
}
10691071
}
10701072

1073+
async updateSignalReportState(
1074+
reportId: string,
1075+
input: {
1076+
state: Extract<SignalReportStatus, "suppressed" | "potential">;
1077+
snooze_for?: number;
1078+
reset_weight?: boolean;
1079+
error?: string;
1080+
},
1081+
): Promise<SignalReport> {
1082+
const teamId = await this.getTeamId();
1083+
const url = new URL(
1084+
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/state/`,
1085+
);
1086+
const path = `/api/projects/${teamId}/signal_reports/${reportId}/state/`;
1087+
1088+
const response = await this.api.fetcher.fetch({
1089+
method: "post",
1090+
url,
1091+
path,
1092+
overrides: {
1093+
body: JSON.stringify(input),
1094+
},
1095+
});
1096+
1097+
if (!response.ok) {
1098+
const errorText = await response.text();
1099+
throw new Error(errorText || "Failed to update signal report state");
1100+
}
1101+
1102+
return (await response.json()) as SignalReport;
1103+
}
1104+
1105+
async deleteSignalReport(reportId: string): Promise<{
1106+
status: "deletion_started" | "already_running";
1107+
report_id: string;
1108+
}> {
1109+
const teamId = await this.getTeamId();
1110+
const url = new URL(
1111+
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/`,
1112+
);
1113+
const path = `/api/projects/${teamId}/signal_reports/${reportId}/`;
1114+
1115+
const response = await this.api.fetcher.fetch({
1116+
method: "delete",
1117+
url,
1118+
path,
1119+
});
1120+
1121+
if (!response.ok) {
1122+
const errorText = await response.text();
1123+
throw new Error(errorText || "Failed to delete signal report");
1124+
}
1125+
1126+
return (await response.json()) as {
1127+
status: "deletion_started" | "already_running";
1128+
report_id: string;
1129+
};
1130+
}
1131+
1132+
async reingestSignalReport(reportId: string): Promise<{
1133+
status: "reingestion_started" | "already_running";
1134+
report_id: string;
1135+
}> {
1136+
const teamId = await this.getTeamId();
1137+
const url = new URL(
1138+
`${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/reingest/`,
1139+
);
1140+
const path = `/api/projects/${teamId}/signal_reports/${reportId}/reingest/`;
1141+
1142+
const response = await this.api.fetcher.fetch({
1143+
method: "post",
1144+
url,
1145+
path,
1146+
});
1147+
1148+
if (!response.ok) {
1149+
const errorText = await response.text();
1150+
throw new Error(errorText || "Failed to reingest signal report");
1151+
}
1152+
1153+
return (await response.json()) as {
1154+
status: "reingestion_started" | "already_running";
1155+
report_id: string;
1156+
};
1157+
}
1158+
10711159
async getMcpServers(): Promise<McpRecommendedServer[]> {
10721160
const teamId = await this.getTeamId();
10731161
const url = new URL(

apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail";
88
import { InboxSourcesDialog } from "@features/inbox/components/InboxSourcesDialog";
99
import { useInboxReportsInfinite } from "@features/inbox/hooks/useInboxReports";
1010
import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs";
11+
import { useInboxReportSelectionStore } from "@features/inbox/stores/inboxReportSelectionStore";
1112
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
1213
import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore";
1314
import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore";
@@ -107,6 +108,13 @@ export function InboxSignalsTab() {
107108

108109
// ── Selection state ─────────────────────────────────────────────────────
109110
const [selectedReportId, setSelectedReportId] = useState<string | null>(null);
111+
const selectedReportIds = useInboxReportSelectionStore(
112+
(s) => s.selectedReportIds ?? [],
113+
);
114+
const toggleReportSelection = useInboxReportSelectionStore(
115+
(s) => s.toggleReportSelection,
116+
);
117+
const pruneSelection = useInboxReportSelectionStore((s) => s.pruneSelection);
110118

111119
useEffect(() => {
112120
if (reports.length === 0) {
@@ -124,6 +132,10 @@ export function InboxSignalsTab() {
124132
}
125133
}, [reports, selectedReportId]);
126134

135+
useEffect(() => {
136+
pruneSelection(reports.map((report) => report.id));
137+
}, [reports, pruneSelection]);
138+
127139
const selectedReport = useMemo(
128140
() => reports.find((report) => report.id === selectedReportId) ?? null,
129141
[reports, selectedReportId],
@@ -201,19 +213,24 @@ export function InboxSignalsTab() {
201213
selectedReportIdRef.current = selectedReportId;
202214
const leftPaneRef = useRef<HTMLDivElement>(null);
203215

216+
const focusListPane = useCallback(() => {
217+
requestAnimationFrame(() => {
218+
leftPaneRef.current?.focus();
219+
});
220+
}, []);
221+
204222
// Auto-focus the list pane when the two-pane layout appears
205223
useEffect(() => {
206224
if (showTwoPaneLayout) {
207225
// Small delay to ensure the ref is mounted after conditional render
208-
requestAnimationFrame(() => {
209-
leftPaneRef.current?.focus();
210-
});
226+
focusListPane();
211227
}
212-
}, [showTwoPaneLayout]);
228+
}, [focusListPane, showTwoPaneLayout]);
213229

214230
const navigateReport = useCallback((direction: 1 | -1) => {
215231
const list = reportsRef.current;
216232
if (list.length === 0) return;
233+
217234
const currentId = selectedReportIdRef.current;
218235
const currentIndex = currentId
219236
? list.findIndex((r) => r.id === currentId)
@@ -223,10 +240,22 @@ export function InboxSignalsTab() {
223240
? 0
224241
: Math.max(0, Math.min(list.length - 1, currentIndex + direction));
225242
const nextId = list[nextIndex].id;
243+
226244
setSelectedReportId(nextId);
227-
leftPaneRef.current
228-
?.querySelector(`[data-report-id="${nextId}"]`)
229-
?.scrollIntoView({ block: "nearest" });
245+
246+
const container = leftPaneRef.current;
247+
const row = container?.querySelector<HTMLElement>(
248+
`[data-report-id="${nextId}"]`,
249+
);
250+
const stickyHeader = container?.querySelector<HTMLElement>(
251+
"[data-inbox-sticky-header]",
252+
);
253+
254+
if (!row) return;
255+
256+
const stickyHeaderHeight = stickyHeader?.offsetHeight ?? 0;
257+
row.style.scrollMarginTop = `${stickyHeaderHeight}px`;
258+
row.scrollIntoView({ block: "nearest" });
230259
}, []);
231260

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

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

247277
if (e.key === "ArrowDown") {
248278
e.preventDefault();
249279
navigateReport(1);
250280
} else if (e.key === "ArrowUp") {
251281
e.preventDefault();
252282
navigateReport(-1);
283+
} else if (e.key === " " && selectedReportIdRef.current) {
284+
e.preventDefault();
285+
toggleReportSelection(selectedReportIdRef.current);
253286
}
254287
};
255288
window.addEventListener("keydown", handler);
256289
return () => window.removeEventListener("keydown", handler);
257-
}, [navigateReport]);
290+
}, [navigateReport, toggleReportSelection]);
258291

259292
const searchDisabledReason =
260293
!hasReports && !searchQuery.trim()
@@ -287,11 +320,33 @@ export function InboxSignalsTab() {
287320
<Flex
288321
ref={leftPaneRef}
289322
direction="column"
290-
tabIndex={-1}
323+
tabIndex={0}
291324
className="outline-none"
325+
onMouseDownCapture={(e) => {
326+
const target = e.target as HTMLElement;
327+
if (
328+
target.closest(
329+
"[data-report-id], button, input, select, textarea, [role='checkbox']",
330+
)
331+
) {
332+
focusListPane();
333+
}
334+
}}
335+
onFocusCapture={(e) => {
336+
const target = e.target as HTMLElement;
337+
if (
338+
target !== leftPaneRef.current &&
339+
target.closest(
340+
"[data-report-id], button, input, select, textarea, [role='checkbox']",
341+
)
342+
) {
343+
focusListPane();
344+
}
345+
}}
292346
>
293347
<InboxLiveRail active={inboxPollingActive} />
294348
<Box
349+
data-inbox-sticky-header
295350
style={{
296351
position: "sticky",
297352
top: 0,
@@ -306,6 +361,7 @@ export function InboxSignalsTab() {
306361
livePolling={inboxPollingActive}
307362
readyCount={readyCount}
308363
processingCount={processingCount}
364+
reports={reports}
309365
/>
310366
</Box>
311367
<ReportListPane
@@ -322,7 +378,9 @@ export function InboxSignalsTab() {
322378
searchQuery={searchQuery}
323379
hasActiveFilters={hasActiveFilters}
324380
selectedReportId={selectedReportId}
381+
selectedReportIds={selectedReportIds}
325382
onSelectReport={setSelectedReportId}
383+
onToggleReportSelection={toggleReportSelection}
326384
/>
327385
</Flex>
328386
</ScrollArea>

0 commit comments

Comments
 (0)