refactor(inbox): derive from snapshot; delete getInbox IPC chain

Sibling refactor to the sidebar-badges cleanup. The inbox aggregates
five item categories (pending approvals, failed runs, budget warnings,
blocked/review tasks, unread company/incident/urgent messages) — every
row is already in `ProfileSnapshot`. The server-side `getInbox`
aggregation (180 lines of SQL + projection) was a redundant second
path.

New: `src/shared/inbox-items.ts::deriveInboxItems(snapshot, companyId)`
produces the same InboxItem[] shape with the same severity ordering
and 24h failed-run window. Pure function, no IPC, no state.

Deleted:
- database/company-crud.ts::getInbox (180-line aggregation)
- database-base.ts::getInbox pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- state/slices/company.ts: `inboxItems` field, `setInboxItems` action
- useDataLoader.ts: both getInbox call sites (initial snapshot load
  and section-change effect)
- useCompanyController.ts: inbox reset on company switch

OperationsSections now computes `inboxItems` via `useMemo` off the
current snapshot + company id. The snapshot is already company-scoped
at the main/database/snapshot.ts layer when `currentCompanyId` is
non-null; the derivation re-filters defensively so the function works
correctly even for unscoped snapshots.

Tests: 603/603 green. No new tests added — the derivation is a pure
function whose shape is exercised end-to-end by the existing Inbox
component integration.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-18 21:42:58 -04:00
1 parent 487f2456ff
commit 6abc1a823f
11 files changed
+180 -220

No files matched your search

@@ -35,7 +35,6 @@ export function useCompanyController() {
const clearCompanyScopedUiState = useCallback(() => {
const store = useAppStore.getState();
store.clearCompanyScopedSelections();
store.setInboxItems([]);
store.setCompanyMetrics(null);
store.setStandupReport(null);
store.setSocialAccounts([]);
-8
View File
@@ -24,8 +24,6 @@ export function useDataLoader(
if (!currentSelectedConnectorId && next.connectors[0]) store.setSelectedConnectorId(next.connectors[0].id);
if (!currentSelectedRunId && next.runs[0]) store.setSelectedRunId(next.runs[0].id);
if (next.currentCompanyId) {
const inboxResult = await window.agentCompany.getInbox(next.currentCompanyId);
if (inboxResult.ok) store.setInboxItems(inboxResult.data);
store.setSocialAccounts(next.socialAccounts?.filter((a) => a.companyId === next.currentCompanyId) ?? []);
const browserActionResult = await window.agentCompany.listBrowserActions({ companyId: next.currentCompanyId, limit: 50 });
if (browserActionResult.ok) {
@@ -50,12 +48,6 @@ export function useDataLoader(
let cancelled = false;
const store = useAppStore.getState();
if (section === "inbox") {
void window.agentCompany.getInbox(companyId).then((result) => {
if (!cancelled && result.ok) store.setInboxItems(result.data);
});
}
if (section === "performance") {
void window.agentCompany.getCompanyMetrics({ companyId, days: 30 }).then((result) => {
if (!cancelled && result.ok) store.setCompanyMetrics(result.data);
+5 -12
View File
@@ -4,17 +4,17 @@ import type {
AgentRecord,
ApprovalRecord,
CompanyMetrics,
InboxItem,
ProfileSnapshot,
RunRecord,
StandupReport,
TaskRecord,
} from "@shared/types";
// Sidebar badge counts live in the renderer — derived from the
// snapshot on every read via `deriveSidebarBadges` rather than cached
// on this slice. One source of truth is the snapshot; adding a
// separate `badges` field here is what let the counters drift.
// Sidebar badge counts and inbox items both live in the renderer —
// derived on every read from the snapshot (see `@shared/sidebar-badges`
// and `@shared/inbox-items`). The snapshot is the only source of
// truth; caching a separate `badges` or `inboxItems` field on this
// slice is what let the counters drift against the rendered content.
export interface CompanySlice {
snapshot: ProfileSnapshot | null;
/**
@@ -26,12 +26,10 @@ export interface CompanySlice {
snapshotRevision: number;
companyMetrics: CompanyMetrics | null;
standupReport: StandupReport | null;
inboxItems: InboxItem[];
error: string | null;
setSnapshot: (snapshot: ProfileSnapshot | null) => void;
setCompanyMetrics: (metrics: CompanyMetrics | null) => void;
setStandupReport: (report: StandupReport | null) => void;
setInboxItems: (items: InboxItem[]) => void;
setError: (error: string | null) => void;
refreshSnapshot: () => Promise<void>;
patchTask: (task: TaskRecord) => void;
@@ -54,7 +52,6 @@ export const createCompanySlice: StateCreator<
snapshotRevision: 0,
companyMetrics: null,
standupReport: null,
inboxItems: [],
error: null,
setSnapshot: (snapshot) =>
set((state) => {
@@ -69,10 +66,6 @@ export const createCompanySlice: StateCreator<
set((state) => {
state.standupReport = report;
}),
setInboxItems: (items) =>
set((state) => {
state.inboxItems = items;
}),
setError: (error) =>
set((state) => {
state.error = error;
@@ -1,4 +1,6 @@
import { useMemo } from "react";
import type { SectionId } from "@shared/types";
import { deriveInboxItems } from "@shared/inbox-items";
import { Inbox } from "../../components/inbox/Inbox";
import { StandupView } from "../../components/inbox/StandupView";
import { PerformanceView } from "../../components/inbox/PerformanceView";
@@ -47,7 +49,10 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
} = useSectionContext();
const { refresh, openPathWithFeedback, setError } = useAppActions();
const inboxItems = useAppStore((s) => s.inboxItems);
const inboxItems = useMemo(
() => (snapshot ? deriveInboxItems(snapshot, currentCompany?.id ?? null) : []),
[snapshot, currentCompany?.id],
);
const companyMetrics = useAppStore((s) => s.companyMetrics);
const standupReport = useAppStore((s) => s.standupReport);
const socialAccounts = useAppStore((s) => s.socialAccounts);