feat(01-01): migrate App.tsx from 31 useState calls to Zustand store

- Replace all 31 useState declarations with useAppStore selectors
- Add direct setters (setCommandPaletteOpen, setConsoleOpen, etc.) to NavigationSlice
- Replace IPC debounce with createEventBatcher (50ms window)
- Route shortcut events through store.getState() for toggle actions
- Remove unused type imports (SidebarBadges, CompanyMetrics, etc.)
- Zero useState calls remain in App.tsx (down from 31)
- TypeScript compilation passes with zero errors
- All 14 unit tests continue to pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-22 01:36:55 -04:00
1 parent 5e9d36c058
commit 0f72ea4c77
2 files changed
+115 -77

No files matched your search

+91 -77
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { import {
ArrowClockwise, ArrowClockwise,
@@ -38,20 +38,15 @@ import {
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import type { import type {
CompanyRecord, CompanyRecord,
CompanyMetrics,
DesktopEvent, DesktopEvent,
InboxItem,
OnboardingBootstrapResult, OnboardingBootstrapResult,
ProfileSnapshot, ProfileSnapshot,
SectionId, SectionId,
SocialAccountRecord,
BrowserActionRecord,
StandupReport,
} from "@shared/types"; } from "@shared/types";
import type { AppLocale } from "@shared/locale"; import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
import type { SidebarBadges } from "@shared/contracts";
import { createTranslator, LocaleContext, TranslationContext } from "./i18n"; import { createTranslator, LocaleContext, TranslationContext } from "./i18n";
import { useAppStore } from "./state/store";
import { createEventBatcher } from "./lib/ipc-batcher";
import { CompanyOnboarding } from "./components/CompanyOnboarding"; import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { Inbox, StandupView, PerformanceView } from "./components/inbox"; import { Inbox, StandupView, PerformanceView } from "./components/inbox";
import { ConnectorEditor } from "./components/connectors"; import { ConnectorEditor } from "./components/connectors";
@@ -167,7 +162,9 @@ export function App() {
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const }; const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const }; const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };
const [locale, setLocale] = useState<AppLocale>(DEFAULT_LOCALE); // -- Zustand store bindings (replaces 31 useState calls) --
const locale = useAppStore((s) => s.locale);
const setLocale = useAppStore((s) => s.setLocale);
const t = useMemo(() => createTranslator(locale), [locale]); const t = useMemo(() => createTranslator(locale), [locale]);
const navGroups: NavGroup[] = useMemo(() => navGroupDefs.map((group) => ({ const navGroups: NavGroup[] = useMemo(() => navGroupDefs.map((group) => ({
@@ -176,52 +173,74 @@ export function App() {
})), [t]); })), [t]);
const navItems: NavItem[] = useMemo(() => navGroups.flatMap((group) => group.items).concat({ id: "settings", label: t("nav.settings"), icon: Gear }), [navGroups, t]); const navItems: NavItem[] = useMemo(() => navGroups.flatMap((group) => group.items).concat({ id: "settings", label: t("nav.settings"), icon: Gear }), [navGroups, t]);
const [snapshot, setSnapshot] = useState<ProfileSnapshot | null>(null); const snapshot = useAppStore((s) => s.snapshot);
const [section, setSection] = useState<SectionId>("overview"); const setSnapshot = useAppStore((s) => s.setSnapshot);
const [error, setError] = useState<string | null>(null); const section = useAppStore((s) => s.section);
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); const setSection = useAppStore((s) => s.setSection);
const [consoleOpen, setConsoleOpen] = useState(false); const error = useAppStore((s) => s.error);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const setError = useAppStore((s) => s.setError);
const [companySwitcherOpen, setCompanySwitcherOpen] = useState(false); const commandPaletteOpen = useAppStore((s) => s.commandPaletteOpen);
const [confirmDeleteCompanyId, setConfirmDeleteCompanyId] = useState<string | null>(null); const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null); const consoleOpen = useAppStore((s) => s.consoleOpen);
const [selectedConnectorId, setSelectedConnectorId] = useState<string | null>(null); const setConsoleOpen = useAppStore((s) => s.setConsoleOpen);
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null); const sidebarCollapsed = useAppStore((s) => s.sidebarCollapsed);
const [selectedRunId, setSelectedRunId] = useState<string | null>(null); const setSidebarCollapsed = useAppStore((s) => s.setSidebarCollapsed);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null); const companySwitcherOpen = useAppStore((s) => s.companySwitcherOpen);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null); const setCompanySwitcherOpen = useAppStore((s) => s.setCompanySwitcherOpen);
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null); const confirmDeleteCompanyId = useAppStore((s) => s.confirmDeleteCompanyId);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(null); const setConfirmDeleteCompanyId = useAppStore((s) => s.setConfirmDeleteCompanyId);
const [selectedCommunicationEntityId, setSelectedCommunicationEntityId] = useState<string | null>(null); const selectedTaskId = useAppStore((s) => s.selectedTaskId);
const [taskViewMode, setTaskViewMode] = useState<"list" | "kanban">("list"); const setSelectedTaskId = useAppStore((s) => s.setSelectedTaskId);
const selectedConnectorId = useAppStore((s) => s.selectedConnectorId);
const setSelectedConnectorId = useAppStore((s) => s.setSelectedConnectorId);
const selectedApprovalId = useAppStore((s) => s.selectedApprovalId);
const setSelectedApprovalId = useAppStore((s) => s.setSelectedApprovalId);
const selectedRunId = useAppStore((s) => s.selectedRunId);
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId);
const selectedGoalId = useAppStore((s) => s.selectedGoalId);
const setSelectedGoalId = useAppStore((s) => s.setSelectedGoalId);
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId);
const setSelectedWorkspaceId = useAppStore((s) => s.setSelectedWorkspaceId);
const selectedCommunicationEntityId = useAppStore((s) => s.selectedCommunicationEntityId);
const setSelectedCommunicationEntityId = useAppStore((s) => s.setSelectedCommunicationEntityId);
const taskViewMode = useAppStore((s) => s.taskViewMode);
const setTaskViewMode = useAppStore((s) => s.setTaskViewMode);
const logBufferRef = useRef<Record<string, string>>({}); const logBufferRef = useRef<Record<string, string>>({});
const [, setLogRevision] = useState(0); const incrementLogRevision = useAppStore((s) => s.incrementLogRevision);
const [badges, setBadges] = useState<SidebarBadges | null>(null); const badges = useAppStore((s) => s.badges);
const [commandPaletteSearch, setCommandPaletteSearch] = useState(""); const setBadges = useAppStore((s) => s.setBadges);
const [selectedPaletteIndex, setSelectedPaletteIndex] = useState(0); const commandPaletteSearch = useAppStore((s) => s.commandPaletteSearch);
const [showOnboarding, setShowOnboarding] = useState(false); const setCommandPaletteSearch = useAppStore((s) => s.setCommandPaletteSearch);
const [initialOnboardingLocked, setInitialOnboardingLocked] = useState(false); const selectedPaletteIndex = useAppStore((s) => s.selectedPaletteIndex);
const [inboxItems, setInboxItems] = useState<InboxItem[]>([]); const setSelectedPaletteIndex = useAppStore((s) => s.setSelectedPaletteIndex);
const [companyMetrics, setCompanyMetrics] = useState<CompanyMetrics | null>(null); const showOnboarding = useAppStore((s) => s.showOnboarding);
const [standupReport, setStandupReport] = useState<StandupReport | null>(null); const setShowOnboarding = useAppStore((s) => s.setShowOnboarding);
const [socialAccounts, setSocialAccounts] = useState<SocialAccountRecord[]>([]); const initialOnboardingLocked = useAppStore((s) => s.initialOnboardingLocked);
const [browserActions, setBrowserActions] = useState<BrowserActionRecord[]>([]); const setInitialOnboardingLocked = useAppStore((s) => s.setInitialOnboardingLocked);
const inboxItems = useAppStore((s) => s.inboxItems);
const setInboxItems = useAppStore((s) => s.setInboxItems);
const companyMetrics = useAppStore((s) => s.companyMetrics);
const setCompanyMetrics = useAppStore((s) => s.setCompanyMetrics);
const standupReport = useAppStore((s) => s.standupReport);
const setStandupReport = useAppStore((s) => s.setStandupReport);
const socialAccounts = useAppStore((s) => s.socialAccounts);
const setSocialAccounts = useAppStore((s) => s.setSocialAccounts);
const browserActions = useAppStore((s) => s.browserActions);
const setBrowserActions = useAppStore((s) => s.setBrowserActions);
const clearCompanyScopedUiState = useCallback(() => { const clearCompanyScopedUiState = useCallback(() => {
setSelectedTaskId(null); const store = useAppStore.getState();
setSelectedApprovalId(null); store.clearCompanyScopedSelections();
setSelectedRunId(null); store.setInboxItems([]);
setSelectedAgentId(null); store.setCompanyMetrics(null);
setSelectedGoalId(null); store.setStandupReport(null);
setSelectedProjectId(null); store.setSocialAccounts([]);
setSelectedWorkspaceId(null); store.setBrowserActions([]);
setSelectedCommunicationEntityId(null); store.setBadges(null);
setInboxItems([]);
setCompanyMetrics(null);
setStandupReport(null);
setSocialAccounts([]);
setBrowserActions([]);
setBadges(null);
}, []); }, []);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
@@ -266,21 +285,17 @@ export function App() {
refreshRef.current = refresh; refreshRef.current = refresh;
}, [refresh]); }, [refresh]);
// Debounce timer ref for coalescing rapid real-time events // IPC event subscription with batcher for coalescing rapid domain-changed events
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedRefresh = useCallback(() => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
void refreshRef.current();
}, 500);
}, []);
useEffect(() => { useEffect(() => {
void refreshRef.current(); void refreshRef.current();
const store = useAppStore;
const batcher = createEventBatcher(() => {
void refreshRef.current();
}, 50);
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => { const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
if (event.type === "run-log") { if (event.type === "run-log") {
// Pass through immediately -- no batching for streaming logs
const MAX_LOG_BYTES_PER_RUN = 512_000; const MAX_LOG_BYTES_PER_RUN = 512_000;
const MAX_TRACKED_RUNS = 50; const MAX_TRACKED_RUNS = 50;
const existing = logBufferRef.current[event.runId] ?? ""; const existing = logBufferRef.current[event.runId] ?? "";
@@ -294,24 +309,23 @@ export function App() {
delete logBufferRef.current[oldId]; delete logBufferRef.current[oldId];
} }
} }
setLogRevision((r) => r + 1); incrementLogRevision();
return; return;
} }
if (event.type === "shortcut") { if (event.type === "shortcut") {
if (event.action === "toggle-command-palette") setCommandPaletteOpen((current) => !current); if (event.action === "toggle-command-palette") store.getState().toggleCommandPalette();
if (event.action === "toggle-console") setConsoleOpen((current) => !current); if (event.action === "toggle-console") store.getState().toggleConsole();
if (event.action === "refresh") void refreshRef.current(); if (event.action === "refresh") void refreshRef.current();
return; return;
} }
// All data-mutating events trigger a debounced snapshot reload: // All data-mutating events: batch the snapshot reload
// domain-changed, run-status, new-message, automation-fired, heartbeat-status batcher.schedule();
debouncedRefresh();
}); });
return () => { return () => {
unsubscribe(); unsubscribe();
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); batcher.dispose();
}; };
}, [debouncedRefresh]); }, []);
useEffect(() => { useEffect(() => {
if (!snapshot) return; if (!snapshot) return;
@@ -402,7 +416,7 @@ export function App() {
const result = await window.agentCompany.getRunLog({ companyId: activeRun.companyId, runId: activeRunId }); const result = await window.agentCompany.getRunLog({ companyId: activeRun.companyId, runId: activeRunId });
if (result.ok) { if (result.ok) {
logBufferRef.current = { ...logBufferRef.current, [activeRunId]: result.data }; logBufferRef.current = { ...logBufferRef.current, [activeRunId]: result.data };
setLogRevision((r) => r + 1); incrementLogRevision();
return; return;
} }
setError(result.error.message); setError(result.error.message);
@@ -514,12 +528,12 @@ export function App() {
if (paletteActions.length === 0) return; if (paletteActions.length === 0) return;
if (event.key === "ArrowDown") { if (event.key === "ArrowDown") {
event.preventDefault(); event.preventDefault();
setSelectedPaletteIndex((current) => (current + 1) % paletteActions.length); setSelectedPaletteIndex((useAppStore.getState().selectedPaletteIndex + 1) % paletteActions.length);
return; return;
} }
if (event.key === "ArrowUp") { if (event.key === "ArrowUp") {
event.preventDefault(); event.preventDefault();
setSelectedPaletteIndex((current) => (current - 1 + paletteActions.length) % paletteActions.length); setSelectedPaletteIndex((useAppStore.getState().selectedPaletteIndex - 1 + paletteActions.length) % paletteActions.length);
return; return;
} }
if (event.key === "Enter") { if (event.key === "Enter") {
@@ -679,7 +693,7 @@ export function App() {
<button <button
type="button" type="button"
className="focus-ring flex w-full items-center gap-2 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1.5 text-left text-[13px] font-medium text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]" className="focus-ring flex w-full items-center gap-2 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1.5 text-left text-[13px] font-medium text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]"
onClick={() => setCompanySwitcherOpen((v) => !v)} onClick={() => setCompanySwitcherOpen(!companySwitcherOpen)}
> >
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span> <span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span>
<CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" /> <CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" />
@@ -794,7 +808,7 @@ export function App() {
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
className={`focus-ring mt-1 flex w-full items-center rounded-[6px] py-[7px] text-[color:var(--muted)] transition duration-150 hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] ${sidebarCollapsed ? "justify-center p-2" : "gap-2.5 px-3"}`} className={`focus-ring mt-1 flex w-full items-center rounded-[6px] py-[7px] text-[color:var(--muted)] transition duration-150 hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] ${sidebarCollapsed ? "justify-center p-2" : "gap-2.5 px-3"}`}
onClick={() => setSidebarCollapsed((c) => !c)} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
> >
<SidebarSimple size={sidebarCollapsed ? 18 : 17} /> <SidebarSimple size={sidebarCollapsed ? 18 : 17} />
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">{t("app.collapse")}</span> : null} {!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">{t("app.collapse")}</span> : null}
@@ -825,7 +839,7 @@ export function App() {
aria-label={consoleOpen ? "Hide console" : "Show console"} aria-label={consoleOpen ? "Hide console" : "Show console"}
title={consoleOpen ? "Hide console" : "Show console"} title={consoleOpen ? "Hide console" : "Show console"}
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]" className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
onClick={() => setConsoleOpen((current) => !current)} onClick={() => setConsoleOpen(!consoleOpen)}
> >
<TerminalWindow size={16} /> <TerminalWindow size={16} />
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span> <span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
+24
View File
@@ -11,11 +11,15 @@ export interface NavigationSlice {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
companySwitcherOpen: boolean; companySwitcherOpen: boolean;
setSection: (section: SectionId) => void; setSection: (section: SectionId) => void;
setCommandPaletteOpen: (open: boolean) => void;
toggleCommandPalette: () => void; toggleCommandPalette: () => void;
setCommandPaletteSearch: (search: string) => void; setCommandPaletteSearch: (search: string) => void;
setSelectedPaletteIndex: (index: number) => void; setSelectedPaletteIndex: (index: number) => void;
setConsoleOpen: (open: boolean) => void;
toggleConsole: () => void; toggleConsole: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebar: () => void; toggleSidebar: () => void;
setCompanySwitcherOpen: (open: boolean) => void;
toggleCompanySwitcher: () => void; toggleCompanySwitcher: () => void;
navigateTo: (section: SectionId, entityId?: string) => void; navigateTo: (section: SectionId, entityId?: string) => void;
} }
@@ -37,6 +41,14 @@ export const createNavigationSlice: StateCreator<
set((state) => { set((state) => {
state.section = section; state.section = section;
}), }),
setCommandPaletteOpen: (open) =>
set((state) => {
state.commandPaletteOpen = open;
if (!open) {
state.commandPaletteSearch = "";
state.selectedPaletteIndex = 0;
}
}),
toggleCommandPalette: () => toggleCommandPalette: () =>
set((state) => { set((state) => {
state.commandPaletteOpen = !state.commandPaletteOpen; state.commandPaletteOpen = !state.commandPaletteOpen;
@@ -54,14 +66,26 @@ export const createNavigationSlice: StateCreator<
set((state) => { set((state) => {
state.selectedPaletteIndex = index; state.selectedPaletteIndex = index;
}), }),
setConsoleOpen: (open) =>
set((state) => {
state.consoleOpen = open;
}),
toggleConsole: () => toggleConsole: () =>
set((state) => { set((state) => {
state.consoleOpen = !state.consoleOpen; state.consoleOpen = !state.consoleOpen;
}), }),
setSidebarCollapsed: (collapsed) =>
set((state) => {
state.sidebarCollapsed = collapsed;
}),
toggleSidebar: () => toggleSidebar: () =>
set((state) => { set((state) => {
state.sidebarCollapsed = !state.sidebarCollapsed; state.sidebarCollapsed = !state.sidebarCollapsed;
}), }),
setCompanySwitcherOpen: (open) =>
set((state) => {
state.companySwitcherOpen = open;
}),
toggleCompanySwitcher: () => toggleCompanySwitcher: () =>
set((state) => { set((state) => {
state.companySwitcherOpen = !state.companySwitcherOpen; state.companySwitcherOpen = !state.companySwitcherOpen;