Skip to content

Commit e2e73b2

Browse files
committed
feat(code): Signals inbox improvements and priority badges
1 parent 12e0124 commit e2e73b2

21 files changed

+870
-90
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface ExternalDataSourceSchema {
3737
id: string;
3838
name: string;
3939
should_sync: boolean;
40+
/** e.g. `full_refresh` (full table replication), `incremental`, `append` */
41+
sync_type?: string | null;
4042
}
4143

4244
export interface ExternalDataSource {
@@ -327,7 +329,7 @@ export class PostHogAPIClient {
327329
async updateExternalDataSchema(
328330
projectId: number,
329331
schemaId: string,
330-
updates: { should_sync: boolean },
332+
updates: { should_sync: boolean; sync_type?: string },
331333
): Promise<void> {
332334
const urlPath = `/api/projects/${projectId}/external_data_schemas/${schemaId}/`;
333335
const url = new URL(`${this.api.baseUrl}${urlPath}`);

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ const REQUIRED_SCHEMAS: Record<DataSourceType, string[]> = {
1414
zendesk: ["tickets"],
1515
};
1616

17+
/** PostHog DWH: full table replication (non-incremental); API enum value `full_refresh`. */
18+
const FULL_TABLE_REPLICATION = "full_refresh" as const;
19+
1720
function schemasPayload(source: DataSourceType) {
1821
return REQUIRED_SCHEMAS[source].map((name) => ({
1922
name,
2023
should_sync: true,
21-
sync_type: "full_refresh" as const,
24+
sync_type: FULL_TABLE_REPLICATION,
2225
}));
2326
}
2427

@@ -64,13 +67,20 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
6467
}, [repo, repositories]);
6568

6669
const handleSubmit = useCallback(async () => {
67-
if (!projectId || !client || !repo) return;
70+
if (!projectId || !client || !repo || !githubIntegration) return;
6871

6972
setLoading(true);
7073
try {
7174
await client.createExternalDataSource(projectId, {
7275
source_type: "Github",
73-
payload: { repository: repo, schemas: schemasPayload("github") },
76+
payload: {
77+
repository: repo,
78+
auth_method: {
79+
selection: "oauth",
80+
github_integration_id: githubIntegration.id,
81+
},
82+
schemas: schemasPayload("github"),
83+
},
7484
});
7585
toast.success("GitHub data source created");
7686
onComplete();
@@ -81,7 +91,7 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
8191
} finally {
8292
setLoading(false);
8393
}
84-
}, [projectId, client, repo, onComplete]);
94+
}, [projectId, client, repo, githubIntegration, onComplete]);
8595

8696
if (!githubIntegration) {
8797
return (
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
interface InboxLiveRailProps {
2+
active: boolean;
3+
}
4+
5+
/**
6+
* Thin “instrument” scan line while live polling is active — industrial / lab aesthetic.
7+
*/
8+
export function InboxLiveRail({ active }: InboxLiveRailProps) {
9+
if (!active) {
10+
return null;
11+
}
12+
13+
return (
14+
<div
15+
className="relative mb-0 h-px w-full overflow-hidden"
16+
style={{ background: "var(--gray-5)" }}
17+
aria-hidden
18+
>
19+
<div
20+
className="absolute inset-y-0 w-[28%] opacity-95"
21+
style={{
22+
background:
23+
"linear-gradient(90deg, transparent, var(--amber-9), var(--orange-9), transparent)",
24+
animation:
25+
"inboxLiveRailSweep 2.1s cubic-bezier(0.45, 0, 0.55, 1) infinite",
26+
}}
27+
/>
28+
</div>
29+
);
30+
}

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

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResizableSidebar } from "@components/ResizableSidebar";
22
import { useAuthStore } from "@features/auth/stores/authStore";
3+
import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail";
34
import {
45
useInboxReportArtefacts,
56
useInboxReportSignals,
@@ -9,10 +10,15 @@ import { useInboxCloudTaskStore } from "@features/inbox/stores/inboxCloudTaskSto
910
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
1011
import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore";
1112
import { buildSignalTaskPrompt } from "@features/inbox/utils/buildSignalTaskPrompt";
13+
import { filterReportsBySearch } from "@features/inbox/utils/filterReports";
1214
import {
13-
buildOrdering,
14-
filterReportsBySearch,
15-
} from "@features/inbox/utils/filterReports";
15+
INBOX_PIPELINE_STATUS_FILTER,
16+
INBOX_REFETCH_INTERVAL_MS,
17+
} from "@features/inbox/utils/inboxConstants";
18+
import {
19+
isReportActionable,
20+
sortInboxPipelineReports,
21+
} from "@features/inbox/utils/inboxSort";
1622
import { useDraftStore } from "@features/message-editor/stores/draftStore";
1723
import { useCreateTask } from "@features/tasks/hooks/useTasks";
1824
import { useFeatureFlag } from "@hooks/useFeatureFlag";
@@ -21,7 +27,6 @@ import {
2127
ArrowSquareOutIcon,
2228
ClockIcon,
2329
Cloud as CloudIcon,
24-
SparkleIcon,
2530
XIcon,
2631
} from "@phosphor-icons/react";
2732
import {
@@ -40,13 +45,22 @@ import type {
4045
SignalReportsQueryParams,
4146
} from "@shared/types";
4247
import { useNavigationStore } from "@stores/navigationStore";
48+
import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore";
4349
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4450
import { toast } from "sonner";
4551
import { SignalsErrorState, SignalsLoadingState } from "./InboxEmptyStates";
52+
import { InboxWarmingUpState } from "./InboxWarmingUpState";
4653
import { ReportCard } from "./ReportCard";
4754
import { SignalCard } from "./SignalCard";
55+
import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge";
56+
import { SignalReportSummaryMarkdown } from "./SignalReportSummaryMarkdown";
4857
import { SignalsToolbar } from "./SignalsToolbar";
4958

59+
const INBOX_QUERY_PARAMS: SignalReportsQueryParams = {
60+
status: INBOX_PIPELINE_STATUS_FILTER,
61+
ordering: "-updated_at",
62+
};
63+
5064
function getArtefactsUnavailableMessage(
5165
reason: SignalReportArtefactsResponse["unavailableReason"],
5266
): string {
@@ -109,13 +123,9 @@ export function InboxSignalsTab() {
109123
const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection);
110124
const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery);
111125

112-
const queryParams = useMemo<SignalReportsQueryParams>(
113-
() => ({
114-
status: "ready",
115-
ordering: buildOrdering(sortField, sortDirection),
116-
}),
117-
[sortField, sortDirection],
118-
);
126+
const windowFocused = useRendererWindowFocusStore((s) => s.focused);
127+
const isInboxView = useNavigationStore((s) => s.view.type === "inbox");
128+
const inboxPollingActive = windowFocused && isInboxView;
119129

120130
const {
121131
allReports,
@@ -127,10 +137,23 @@ export function InboxSignalsTab() {
127137
hasNextPage,
128138
isFetchingNextPage,
129139
fetchNextPage,
130-
} = useInboxReportsInfinite(queryParams);
131-
const reports = useMemo(
132-
() => filterReportsBySearch(allReports, searchQuery),
133-
[allReports, searchQuery],
140+
} = useInboxReportsInfinite(INBOX_QUERY_PARAMS, {
141+
refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false,
142+
refetchIntervalInBackground: false,
143+
staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 12_000,
144+
});
145+
const reports = useMemo(() => {
146+
const searched = filterReportsBySearch(allReports, searchQuery);
147+
return sortInboxPipelineReports(searched, sortField, sortDirection);
148+
}, [allReports, searchQuery, sortField, sortDirection]);
149+
150+
const readyCount = useMemo(
151+
() => allReports.filter((r) => r.status === "ready").length,
152+
[allReports],
153+
);
154+
const processingCount = useMemo(
155+
() => allReports.filter((r) => r.status !== "ready").length,
156+
[allReports],
134157
);
135158
const [selectedReportId, setSelectedReportId] = useState<string | null>(null);
136159
const sidebarOpen = useInboxSignalsSidebarStore((state) => state.open);
@@ -185,6 +208,9 @@ export function InboxSignalsTab() {
185208
});
186209
const signals = signalsQuery.data?.signals ?? [];
187210

211+
const canActOnReport =
212+
!!selectedReport && isReportActionable(selectedReport.status);
213+
188214
const cloudRegion = useAuthStore((state) => state.cloudRegion);
189215
const projectId = useAuthStore((state) => state.projectId);
190216
const replayBaseUrl =
@@ -217,6 +243,9 @@ export function InboxSignalsTab() {
217243
}, [selectedReport, visibleArtefacts, signals, replayBaseUrl]);
218244

219245
const handleCreateTask = () => {
246+
if (!selectedReport || !isReportActionable(selectedReport.status)) {
247+
return;
248+
}
220249
const prompt = buildPrompt();
221250
if (!prompt) return;
222251

@@ -230,14 +259,21 @@ export function InboxSignalsTab() {
230259
openCloudConfirm(repositories[0] ?? null);
231260
}, [repositories, openCloudConfirm]);
232261

262+
const selectedReportRef = useRef(selectedReport);
263+
selectedReportRef.current = selectedReport;
264+
233265
const handleRunCloudTask = useCallback(async () => {
266+
const report = selectedReportRef.current;
267+
if (!report || !isReportActionable(report.status)) {
268+
return;
269+
}
234270
const prompt = buildPrompt();
235271
if (!prompt) return;
236272

237273
const result = await runCloudTask({
238274
prompt,
239275
githubIntegrationId: githubIntegration?.id,
240-
reportId: selectedReport?.id,
276+
reportId: report.id,
241277
});
242278

243279
if (result.success && result.task) {
@@ -251,7 +287,6 @@ export function InboxSignalsTab() {
251287
runCloudTask,
252288
invalidateTasks,
253289
navigateToTask,
254-
selectedReport?.id,
255290
githubIntegration?.id,
256291
]);
257292

@@ -271,29 +306,7 @@ export function InboxSignalsTab() {
271306
}
272307

273308
if (allReports.length === 0) {
274-
return (
275-
<Flex
276-
direction="column"
277-
align="center"
278-
justify="center"
279-
gap="3"
280-
height="100%"
281-
className="text-center"
282-
>
283-
<SparkleIcon size={24} className="text-gray-8" />
284-
<Text size="2" weight="medium" className="font-mono text-[12px]">
285-
No signals yet
286-
</Text>
287-
<Text
288-
size="1"
289-
color="gray"
290-
className="font-mono text-[11px]"
291-
style={{ maxWidth: 520 }}
292-
>
293-
Signals are processing. Check back soon as fresh events arrive.
294-
</Text>
295-
</Flex>
296-
);
309+
return <InboxWarmingUpState />;
297310
}
298311

299312
return (
@@ -305,10 +318,14 @@ export function InboxSignalsTab() {
305318
style={{ height: "100%" }}
306319
>
307320
<Flex direction="column">
321+
<InboxLiveRail active={inboxPollingActive} />
308322
<SignalsToolbar
309323
totalCount={totalCount}
310324
filteredCount={reports.length}
311325
isSearchActive={!!searchQuery.trim()}
326+
livePolling={inboxPollingActive}
327+
readyCount={readyCount}
328+
processingCount={processingCount}
312329
/>
313330
{reports.length === 0 && searchQuery.trim() ? (
314331
<Flex
@@ -323,9 +340,10 @@ export function InboxSignalsTab() {
323340
</Text>
324341
</Flex>
325342
) : null}
326-
{reports.map((report) => (
343+
{reports.map((report, index) => (
327344
<ReportCard
328345
key={report.id}
346+
index={index}
329347
report={report}
330348
isSelected={selectedReport?.id === report.id}
331349
onClick={() => {
@@ -379,11 +397,12 @@ export function InboxSignalsTab() {
379397
<XIcon size={14} />
380398
</button>
381399
</Flex>
382-
<Flex align="center" gap="1">
400+
<Flex align="center" gap="1" wrap="wrap">
383401
<Button
384402
size="1"
385403
variant="soft"
386404
onClick={handleCreateTask}
405+
disabled={!canActOnReport}
387406
className="font-mono text-[11px]"
388407
>
389408
Create task
@@ -393,14 +412,29 @@ export function InboxSignalsTab() {
393412
size="1"
394413
variant="solid"
395414
onClick={handleOpenCloudConfirm}
396-
disabled={isRunningCloudTask || repositories.length === 0}
415+
disabled={
416+
!canActOnReport ||
417+
isRunningCloudTask ||
418+
repositories.length === 0
419+
}
397420
className="font-mono text-[11px]"
398421
>
399422
<CloudIcon size={12} />
400423
{isRunningCloudTask ? "Running..." : "Run cloud"}
401424
</Button>
402425
)}
403426
</Flex>
427+
{!canActOnReport && selectedReport ? (
428+
<Text
429+
size="1"
430+
color="gray"
431+
className="font-mono text-[10px] leading-snug"
432+
>
433+
{selectedReport.status === "pending_input"
434+
? "This report needs input in PostHog before an agent can act on it."
435+
: "Research is still running — you can read context below, then create a task when status is Ready."}
436+
</Text>
437+
) : null}
404438
</Flex>
405439
<ScrollArea
406440
type="auto"
@@ -409,14 +443,13 @@ export function InboxSignalsTab() {
409443
style={{ height: "calc(100% - 41px)" }}
410444
>
411445
<Flex direction="column" gap="2" p="2" className="min-w-0">
412-
<Text
413-
size="1"
414-
color="gray"
415-
className="whitespace-pre-wrap text-pretty break-words font-mono text-[11px]"
416-
>
417-
{selectedReport.summary ?? "No summary available."}
418-
</Text>
446+
<SignalReportSummaryMarkdown
447+
content={selectedReport.summary}
448+
fallback="No summary available."
449+
variant="detail"
450+
/>
419451
<Flex align="center" gap="2" wrap="wrap">
452+
<SignalReportPriorityBadge priority={selectedReport.priority} />
420453
<Badge variant="soft" color="gray" size="1">
421454
{selectedReport.signal_count} occurrences
422455
</Badge>

0 commit comments

Comments
 (0)