diff --git a/apps/code/src/renderer/assets/images/explorer-hog.png b/apps/code/src/renderer/assets/images/explorer-hog.png
new file mode 100644
index 000000000..95df75d4c
Binary files /dev/null and b/apps/code/src/renderer/assets/images/explorer-hog.png differ
diff --git a/apps/code/src/renderer/assets/images/graphs-hog.png b/apps/code/src/renderer/assets/images/graphs-hog.png
new file mode 100644
index 000000000..fa0b1e9c7
Binary files /dev/null and b/apps/code/src/renderer/assets/images/graphs-hog.png differ
diff --git a/apps/code/src/renderer/assets/images/mail-hog.png b/apps/code/src/renderer/assets/images/mail-hog.png
new file mode 100644
index 000000000..9f95210b0
Binary files /dev/null and b/apps/code/src/renderer/assets/images/mail-hog.png differ
diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx
index 098edeecc..432b9637c 100644
--- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx
+++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx
@@ -1,4 +1,3 @@
-import { ResizableSidebar } from "@components/ResizableSidebar";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail";
import {
@@ -13,21 +12,24 @@ import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignals
import { buildSignalTaskPrompt } from "@features/inbox/utils/buildSignalTaskPrompt";
import {
buildSignalReportListOrdering,
+ buildStatusFilterParam,
filterReportsBySearch,
} from "@features/inbox/utils/filterReports";
-import {
- INBOX_PIPELINE_STATUS_FILTER,
- INBOX_REFETCH_INTERVAL_MS,
-} from "@features/inbox/utils/inboxConstants";
+import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants";
import { useDraftStore } from "@features/message-editor/stores/draftStore";
-import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
+import { SignalSourcesSettings } from "@features/settings/components/sections/SignalSourcesSettings";
import { useCreateTask } from "@features/tasks/hooks/useTasks";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
import {
+ ArrowDownIcon,
ArrowSquareOutIcon,
+ ArrowsClockwiseIcon,
+ CircleNotchIcon,
ClockIcon,
Cloud as CloudIcon,
+ GithubLogoIcon,
+ WarningIcon,
XIcon,
} from "@phosphor-icons/react";
import {
@@ -35,23 +37,27 @@ import {
Badge,
Box,
Button,
+ Dialog,
Flex,
ScrollArea,
Select,
Text,
+ Tooltip,
} from "@radix-ui/themes";
+import explorerHog from "@renderer/assets/images/explorer-hog.png";
+import graphsHog from "@renderer/assets/images/graphs-hog.png";
+import mailHog from "@renderer/assets/images/mail-hog.png";
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
import type {
SignalReportArtefact,
SignalReportArtefactsResponse,
SignalReportsQueryParams,
+ SuggestedReviewersArtefact,
} from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
-import { SignalsErrorState, SignalsLoadingState } from "./InboxEmptyStates";
-import { InboxWarmingUpState } from "./InboxWarmingUpState";
import { ReportCard } from "./ReportCard";
import { SignalCard } from "./SignalCard";
import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge";
@@ -115,13 +121,187 @@ function LoadMoreTrigger({
);
}
+// ── Animated ellipsis for warming-up inline text ─────────────────────────────
+
+function AnimatedEllipsis() {
+ return (
+
+
+ .
+ .
+ .
+
+
+ );
+}
+
+// ── Right pane empty states ─────────────────────────────────────────────────
+
+function WelcomePane({ onEnableInbox }: { onEnableInbox: () => void }) {
+ return (
+
+
+
+
+
+ Welcome to your Inbox
+
+
+
+
+
+ Background analysis of your data — while you sleep.
+
+
+ Session recordings watched automatically. Issues, tickets, and evals
+ analyzed around the clock.
+
+
+
+
+
+
+ Ready-to-run fixes for real user problems.
+
+
+ Each report includes evidence and impact numbers — just execute the
+ prompt in your agent.
+
+
+
+
+
+
+ );
+}
+
+function WarmingUpPane({
+ onConfigureSources,
+}: {
+ onConfigureSources: () => void;
+}) {
+ return (
+
+
+
+
+
+ Inbox is warming up
+
+
+
+
+ Reports will appear here as soon as signals come in.
+
+
+
+
+
+ );
+}
+
+function SelectReportPane() {
+ return (
+
+
+
+
+ Select a report
+
+
+ Pick a report from the list to see details, signals, and evidence.
+
+
+
+ );
+}
+
+// ── Main component ──────────────────────────────────────────────────────────
+
export function InboxSignalsTab() {
const sortField = useInboxSignalsFilterStore((s) => s.sortField);
const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection);
const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery);
+ const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter);
const { data: signalSourceConfigs } = useSignalSourceConfigs();
const hasSignalSources = signalSourceConfigs?.some((c) => c.enabled) ?? false;
- const openSettings = useSettingsDialogStore((s) => s.open);
+ const [sourcesDialogOpen, setSourcesDialogOpen] = useState(false);
const windowFocused = useRendererWindowFocusStore((s) => s.focused);
const isInboxView = useNavigationStore((s) => s.view.type === "inbox");
@@ -129,10 +309,10 @@ export function InboxSignalsTab() {
const inboxQueryParams = useMemo(
(): SignalReportsQueryParams => ({
- status: INBOX_PIPELINE_STATUS_FILTER,
+ status: buildStatusFilterParam(statusFilter),
ordering: buildSignalReportListOrdering(sortField, sortDirection),
}),
- [sortField, sortDirection],
+ [statusFilter, sortField, sortDirection],
);
const {
@@ -164,12 +344,10 @@ export function InboxSignalsTab() {
[allReports],
);
const [selectedReportId, setSelectedReportId] = useState(null);
- const sidebarOpen = useInboxSignalsSidebarStore((state) => state.open);
const sidebarWidth = useInboxSignalsSidebarStore((state) => state.width);
const sidebarIsResizing = useInboxSignalsSidebarStore(
(state) => state.isResizing,
);
- const setSidebarOpen = useInboxSignalsSidebarStore((state) => state.setOpen);
const setSidebarWidth = useInboxSignalsSidebarStore(
(state) => state.setWidth,
);
@@ -190,9 +368,8 @@ export function InboxSignalsTab() {
);
if (!selectedExists) {
setSelectedReportId(null);
- setSidebarOpen(false);
}
- }, [reports, selectedReportId, setSidebarOpen]);
+ }, [reports, selectedReportId]);
const selectedReport = useMemo(
() => reports.find((report) => report.id === selectedReportId) ?? null,
@@ -202,9 +379,16 @@ export function InboxSignalsTab() {
const artefactsQuery = useInboxReportArtefacts(selectedReport?.id ?? "", {
enabled: !!selectedReport,
});
- const visibleArtefacts = (artefactsQuery.data?.results ?? []).filter(
- (a): a is SignalReportArtefact => a.type === "video_segment",
+ const allArtefacts = artefactsQuery.data?.results ?? [];
+ const visibleArtefacts = allArtefacts.filter(
+ (a): a is SignalReportArtefact => a.type !== "suggested_reviewers",
);
+ const suggestedReviewers = useMemo(() => {
+ const reviewerArtefact = allArtefacts.find(
+ (a): a is SuggestedReviewersArtefact => a.type === "suggested_reviewers",
+ );
+ return reviewerArtefact?.content ?? [];
+ }, [allArtefacts]);
const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason;
const showArtefactsUnavailable =
!artefactsQuery.isLoading &&
@@ -299,302 +483,507 @@ export function InboxSignalsTab() {
githubIntegration?.id,
]);
- if (isLoading) {
- return ;
- }
+ // Resize handle for left pane
+ const containerRef = useRef(null);
- if (error) {
- return (
- {
- void refetch();
- }}
- isRetrying={isFetching}
- />
- );
- }
+ const handleResizeMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ setSidebarIsResizing(true);
+ document.body.style.cursor = "col-resize";
+ document.body.style.userSelect = "none";
+ },
+ [setSidebarIsResizing],
+ );
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (!sidebarIsResizing || !containerRef.current) return;
+ const containerLeft = containerRef.current.getBoundingClientRect().left;
+ const containerWidth = containerRef.current.offsetWidth;
+ const maxWidth = containerWidth * 0.6;
+ const newWidth = Math.max(
+ 220,
+ Math.min(maxWidth, e.clientX - containerLeft),
+ );
+ setSidebarWidth(newWidth);
+ };
+ const handleMouseUp = () => {
+ if (sidebarIsResizing) {
+ setSidebarIsResizing(false);
+ document.body.style.cursor = "";
+ document.body.style.userSelect = "";
+ }
+ };
+ document.addEventListener("mousemove", handleMouseMove);
+ document.addEventListener("mouseup", handleMouseUp);
+ return () => {
+ document.removeEventListener("mousemove", handleMouseMove);
+ document.removeEventListener("mouseup", handleMouseUp);
+ };
+ }, [sidebarIsResizing, setSidebarWidth, setSidebarIsResizing]);
+
+ // ── Layout mode: full-width empty state vs two-pane ─────────────────────
- if (allReports.length === 0) {
- if (!hasSignalSources) {
- return (
+ const hasReports = allReports.length > 0;
+ const showTwoPaneLayout = hasReports || !!searchQuery.trim();
+
+ // ── Determine right pane content (only used in two-pane mode) ──────────
+
+ let rightPaneContent: React.ReactNode;
+
+ if (selectedReport) {
+ rightPaneContent = (
+ <>
-
+
- Enable Inbox
-
-
- Inbox automatically analyzes your product data and prioritizes
- actionable tasks. Choose which sources to enable for this project.
+ {selectedReport.title ?? "Untitled signal"}
+
-
+
+
+ {cloudModeEnabled && (
+
+ )}
+
+ {!canActOnReport && selectedReport ? (
+
+ {selectedReport.status === "pending_input"
+ ? "This report needs input in PostHog before an agent can act on it."
+ : "Research is still running — you can read context below, then create a task when status is Ready."}
+
+ ) : null}
- );
- }
- return ;
- }
-
- return (
-
-
-
-
-
- {reports.length === 0 && searchQuery.trim() ? (
-
-
- No matching signals
-
-
- ) : null}
- {reports.map((report, index) => (
- {
- setSelectedReportId(report.id);
- setSidebarOpen(true);
- }}
- />
- ))}
-
+
-
-
-
-
-
- {selectedReport ? (
- <>
-
-
+
+
+
+ {selectedReport.signal_count} occurrences
+
+
+ {selectedReport.relevant_user_count ?? 0} affected users
+
+
+
+ {suggestedReviewers.length > 0 && (
+
- {selectedReport.title ?? "Untitled signal"}
+ Suggested reviewers
-
-
-
-
- {cloudModeEnabled && (
-
- )}
-
- {!canActOnReport && selectedReport ? (
+
+ {suggestedReviewers.map((reviewer) => (
+
+
+
+ {reviewer.user?.first_name ??
+ reviewer.github_name ??
+ reviewer.github_login}
+
+
+ @{reviewer.github_login}
+
+
+ ))}
+
+
+ )}
+
+ {signals.length > 0 && (
+
- {selectedReport.status === "pending_input"
- ? "This report needs input in PostHog before an agent can act on it."
- : "Research is still running — you can read context below, then create a task when status is Ready."}
+ Signals ({signals.length})
- ) : null}
-
-
-
-
-
-
-
- {selectedReport.signal_count} occurrences
-
-
- {selectedReport.relevant_user_count ?? 0} affected users
-
+
+ {signals.map((signal) => (
+
+ ))}
+
+ )}
+ {signalsQuery.isLoading && (
+
+ Loading signals...
+
+ )}
+
+
+
+ Evidence
+
+ {artefactsQuery.isLoading && (
+
+ Loading evidence...
+
+ )}
+ {showArtefactsUnavailable && (
+
+ {artefactsUnavailableMessage}
+
+ )}
+ {!artefactsQuery.isLoading &&
+ !showArtefactsUnavailable &&
+ visibleArtefacts.length === 0 && (
+
+ No artefacts were returned for this signal.
+
+ )}
- {signals.length > 0 && (
-
+
+ {visibleArtefacts.map((artefact) => (
+
- Signals ({signals.length})
+ {artefact.content.content}
-
- {signals.map((signal) => (
-
- ))}
+
+
+
+
+ {artefact.content.start_time
+ ? new Date(
+ artefact.content.start_time,
+ ).toLocaleString()
+ : "Unknown time"}
+
+
+ {replayBaseUrl && artefact.content.session_id && (
+
+ View replay
+
+
+ )}
- )}
- {signalsQuery.isLoading && (
-
- Loading signals...
-
- )}
+ ))}
+
+
+
+
+ >
+ );
+ } else {
+ rightPaneContent = ;
+ }
-
-
- Evidence
-
- {artefactsQuery.isLoading && (
-
- Loading evidence...
-
- )}
- {showArtefactsUnavailable && (
-
- {artefactsUnavailableMessage}
-
- )}
- {!artefactsQuery.isLoading &&
- !showArtefactsUnavailable &&
- visibleArtefacts.length === 0 && (
-
- No session segments available for this report.
-
- )}
+ // ── Left pane content ───────────────────────────────────────────────────
-
- {visibleArtefacts.map((artefact) => (
-
-
- {artefact.content.content}
-
-
-
-
-
- {artefact.content.start_time
- ? new Date(
- artefact.content.start_time,
- ).toLocaleString()
- : "Unknown time"}
-
-
- {replayBaseUrl && artefact.content.session_id && (
-
- View replay
-
-
- )}
-
-
- ))}
-
-
+ let leftPaneList: React.ReactNode;
+
+ if (isLoading && allReports.length === 0 && hasSignalSources) {
+ leftPaneList = (
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+
+
+
+ ))}
+
+ );
+ } else if (error) {
+ leftPaneList = (
+
+
+
+
+ Could not load signals
+
+
+
+
+ );
+ } else if (reports.length === 0 && searchQuery.trim()) {
+ leftPaneList = (
+
+
+ No matching signals
+
+
+ );
+ } else {
+ leftPaneList = (
+ <>
+ {reports.map((report, index) => (
+ setSelectedReportId(report.id)}
+ />
+ ))}
+
+ >
+ );
+ }
+
+ // ── Skeleton rows for backdrop behind empty states ──────────────────────
+
+ const skeletonBackdrop = (
+
+ {Array.from({ length: 8 }).map((_, index) => (
+
+
+
+
+ ))}
+
+ );
+
+ const searchDisabledReason =
+ !hasReports && !searchQuery.trim()
+ ? "No reports in the project\u2026 yet"
+ : null;
+
+ return (
+ <>
+ {showTwoPaneLayout ? (
+
+ {/* ── Left pane: report list ───────────────────────────────── */}
+
+
+
+
+
+ {leftPaneList}
- >
- ) : null}
-
+ {/* Resize handle */}
+
+
+
+ {/* ── Right pane: detail ───────────────────────────────────── */}
+
+ {rightPaneContent}
+
+
+ ) : (
+ /* ── Full-width empty state with skeleton backdrop ──────────── */
+
+
+
+ {skeletonBackdrop}
+
+
+ {!hasSignalSources ? (
+ setSourcesDialogOpen(true)} />
+ ) : (
+ setSourcesDialogOpen(true)}
+ />
+ )}
+
+
+ )}
+
+ {/* ── Sources config dialog ──────────────────────────────────── */}
+
+
+
+
+ Signal sources
+
+
+
+
+
+
+
+ {hasSignalSources ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {/* ── Cloud task confirmation dialog ────────────────────────── */}
{
@@ -663,6 +1052,6 @@ export function InboxSignalsTab() {
-
+ >
);
}
diff --git a/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx b/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx
index 427ad723d..c2ef1bdba 100644
--- a/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx
+++ b/apps/code/src/renderer/features/inbox/components/InboxWarmingUpState.tsx
@@ -1,33 +1,32 @@
import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs";
-import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import {
- BrainIcon,
+ BugIcon,
GithubLogoIcon,
KanbanIcon,
SparkleIcon,
TicketIcon,
VideoIcon,
} from "@phosphor-icons/react";
-import { Flex, Text, Tooltip } from "@radix-ui/themes";
+import { Button, Flex, Text, Tooltip } from "@radix-ui/themes";
import type { SignalSourceConfig } from "@renderer/api/posthogClient";
-import { motion } from "framer-motion";
+import explorerHog from "@renderer/assets/images/explorer-hog.png";
import { type ReactNode, useMemo } from "react";
const SOURCE_DISPLAY_ORDER: SignalSourceConfig["source_product"][] = [
"session_replay",
- "llm_analytics",
+ "error_tracking",
"github",
"linear",
"zendesk",
];
function sourceIcon(product: SignalSourceConfig["source_product"]): ReactNode {
- const common = { size: 22 as const };
+ const common = { size: 20 as const };
switch (product) {
case "session_replay":
return ;
- case "llm_analytics":
- return ;
+ case "error_tracking":
+ return ;
case "github":
return ;
case "linear":
@@ -39,6 +38,25 @@ function sourceIcon(product: SignalSourceConfig["source_product"]): ReactNode {
}
}
+function sourceProductTooltipLabel(
+ product: SignalSourceConfig["source_product"],
+): string {
+ switch (product) {
+ case "session_replay":
+ return "PostHog Session Replay";
+ case "error_tracking":
+ return "PostHog Error Tracking";
+ case "github":
+ return "GitHub Issues";
+ case "linear":
+ return "Linear";
+ case "zendesk":
+ return "Zendesk";
+ default:
+ return "Signal source";
+ }
+}
+
function AnimatedEllipsis({ className }: { className?: string }) {
return (
@@ -51,17 +69,29 @@ function AnimatedEllipsis({ className }: { className?: string }) {
);
}
-export function InboxWarmingUpState() {
+interface InboxWarmingUpStateProps {
+ onConfigureSources: () => void;
+}
+
+export function InboxWarmingUpState({
+ onConfigureSources,
+}: InboxWarmingUpStateProps) {
const { data: configs } = useSignalSourceConfigs();
- const openSignalSettings = useSettingsDialogStore((s) => s.open);
- const enabledSources = useMemo(() => {
- const enabled = (configs ?? []).filter((c) => c.enabled);
- return [...enabled].sort(
- (a, b) =>
- SOURCE_DISPLAY_ORDER.indexOf(a.source_product) -
- SOURCE_DISPLAY_ORDER.indexOf(b.source_product),
- );
+ const enabledProducts = useMemo(() => {
+ const seen = new Set();
+ return (configs ?? [])
+ .filter((c) => c.enabled)
+ .sort(
+ (a, b) =>
+ SOURCE_DISPLAY_ORDER.indexOf(a.source_product) -
+ SOURCE_DISPLAY_ORDER.indexOf(b.source_product),
+ )
+ .filter((c) => {
+ if (seen.has(c.source_product)) return false;
+ seen.add(c.source_product);
+ return true;
+ });
}, [configs]);
return (
@@ -69,88 +99,59 @@ export function InboxWarmingUpState() {
direction="column"
align="center"
justify="center"
- gap="4"
height="100%"
- px="4"
- className="text-center"
+ px="5"
+ style={{ margin: "0 auto" }}
>
-
-
-
+
+
-
-
+
Inbox is warming up
+
-
- Reports appear here as soon as signals are grouped. Research usually
- finishes within a minute while we watch your connected sources.
-
-
- Processing signals
-
-
-
- {enabledSources.length > 0 ? (
-
-
+
+
+ {enabledProducts.map((cfg) => (
+
+
+ {sourceIcon(cfg.source_product)}
+
+
+ ))}
+
-
-
-
-
- ) : null}
+ Configure sources
+
+
+
);
}