This commit is contained in:
ZacharyZhang-NY committed 2026-04-04 12:03:50 -04:00
1 parent 66158e222e
commit 67c757f481
314 files changed
+37945 -22962

No files matched your search

+389
View File
@@ -0,0 +1,389 @@
import { useCallback, useDeferredValue, useEffect, useMemo, useRef } from "react";
import type {
CompanyRecord,
OnboardingBootstrapResult,
SectionId,
} from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { createTranslator } from "../i18n";
import { useAppStore } from "../state/store";
import { unwrap } from "../lib/desktop";
import { useFocusTrap, useReducedMotion } from "../components/ui";
import {
BookOpen,
Gear,
Tray,
SquaresFour,
UsersThree,
ListChecks,
ChatCircle,
PuzzlePiece,
FileText,
ShieldCheck,
Megaphone,
FolderOpen,
} from "@phosphor-icons/react";
import { useDesktopEvents } from "./useDesktopEvents";
import { useDataLoader } from "./useDataLoader";
import { useCompanyData } from "./useCompanyData";
import { useCommandPaletteHotkeys } from "./useCommandPaletteHotkeys";
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
export type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
export type PaletteAction = { key: string; section: SectionId; entityId?: string };
const sidebarNavDefs: NavItemDef[] = [
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
{ id: "inbox", labelKey: "nav.inbox", icon: Tray },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
];
const paletteOnlyNavDefs: NavItemDef[] = [
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
];
export function useAppController() {
const reducedMotion = useReducedMotion();
const noMotion = { duration: 0 };
const pageTransition = reducedMotion ? noMotion : { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const };
const locale = useAppStore((s) => s.locale);
const setLocale = useAppStore((s) => s.setLocale);
const t = useMemo(() => createTranslator(locale), [locale]);
const sidebarNav: NavItem[] = useMemo(() => sidebarNavDefs.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })), [t]);
const navItems: NavItem[] = useMemo(
() =>
sidebarNav
.concat(
paletteOnlyNavDefs.map((item) => ({
id: item.id,
label: t(item.labelKey),
icon: item.icon,
})),
)
.concat({ id: "settings", label: t("nav.settings"), icon: Gear }),
[sidebarNav, t],
);
const snapshot = useAppStore((s) => s.snapshot);
const setSnapshot = useAppStore((s) => s.setSnapshot);
const section = useAppStore((s) => s.section);
const setSection = useAppStore((s) => s.setSection);
const error = useAppStore((s) => s.error);
const setError = useAppStore((s) => s.setError);
const commandPaletteOpen = useAppStore((s) => s.commandPaletteOpen);
const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
const consoleOpen = useAppStore((s) => s.consoleOpen);
const setConsoleOpen = useAppStore((s) => s.setConsoleOpen);
const sidebarCollapsed = useAppStore((s) => s.sidebarCollapsed);
const setSidebarCollapsed = useAppStore((s) => s.setSidebarCollapsed);
const companySwitcherOpen = useAppStore((s) => s.companySwitcherOpen);
const setCompanySwitcherOpen = useAppStore((s) => s.setCompanySwitcherOpen);
const confirmDeleteCompanyId = useAppStore((s) => s.confirmDeleteCompanyId);
const setConfirmDeleteCompanyId = useAppStore((s) => s.setConfirmDeleteCompanyId);
const selectedTaskId = useAppStore((s) => s.selectedTaskId);
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 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 incrementLogRevision = useAppStore((s) => s.incrementLogRevision);
const commandPaletteSearch = useAppStore((s) => s.commandPaletteSearch);
const setCommandPaletteSearch = useAppStore((s) => s.setCommandPaletteSearch);
const selectedPaletteIndex = useAppStore((s) => s.selectedPaletteIndex);
const setSelectedPaletteIndex = useAppStore((s) => s.setSelectedPaletteIndex);
const showOnboarding = useAppStore((s) => s.showOnboarding);
const setShowOnboarding = useAppStore((s) => s.setShowOnboarding);
const initialOnboardingLocked = useAppStore((s) => s.initialOnboardingLocked);
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 setBadges = useAppStore((s) => s.setBadges);
const currentCompany = useMemo<CompanyRecord | null>(() => {
if (!snapshot) return null;
return snapshot.companies.find((company) => company.id === snapshot.currentCompanyId) ?? snapshot.companies[0] ?? null;
}, [snapshot]);
const companyData = useCompanyData(snapshot, currentCompany);
const { tasks, runs, approvals, pendingApprovals, companyAgents, companyGoals, companyProjects,
companyWorkspaces, companyCosts, filteredActivity, companySecrets, companyDocuments,
companyKnowledge, companyMeetings, companySprints, selectedTask, selectedApproval,
selectedRun, selectedAgent, selectedGoal, selectedProject, selectedWorkspace, navBadges } = companyData;
const selectedConnector = snapshot?.connectors.find((connector) => connector.id === selectedConnectorId) ?? snapshot?.connectors[0] ?? null;
const clearCompanyScopedUiState = useCallback(() => {
const store = useAppStore.getState();
store.clearCompanyScopedSelections();
store.setInboxItems([]);
store.setCompanyMetrics(null);
store.setStandupReport(null);
store.setSocialAccounts([]);
store.setBrowserActions([]);
store.setBadges(null);
store.setSkills([]);
store.setSelectedSkillId(null);
store.setSelectedSkillFilePath("SKILL.md");
store.setSkillImportModalOpen(false);
}, []);
const { refresh } = useDataLoader(
snapshot, section, selectedConnectorId, selectedRunId,
setSnapshot, setError, setInitialOnboardingLocked,
setSelectedConnectorId, setSelectedRunId,
setBadges, setInboxItems, setSocialAccounts, setBrowserActions,
setCompanyMetrics, setStandupReport,
);
// Stable ref so the subscribe callback always calls the latest refresh
// without tearing down the subscription when refresh identity changes.
const refreshRef = useRef(refresh);
useEffect(() => { refreshRef.current = refresh; }, [refresh]);
useDesktopEvents(refreshRef, logBufferRef, incrementLogRevision);
useEffect(() => {
if (!snapshot) return;
const root = document.documentElement;
const applyTheme = () => {
const isDark = snapshot.theme === "dark" ||
(snapshot.theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
root.classList.toggle("dark", isDark);
root.style.colorScheme = isDark ? "dark" : "light";
};
applyTheme();
if (snapshot.locale && snapshot.locale !== locale) setLocale(snapshot.locale);
if (snapshot.theme === "system") {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
mq.addEventListener("change", applyTheme);
return () => mq.removeEventListener("change", applyTheme);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setLocale is a stable Zustand setter
}, [snapshot, locale]);
useEffect(() => {
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
}, [locale]);
useEffect(() => {
if (selectedRunId && !runs.some((run) => run.id === selectedRunId)) {
setSelectedRunId(runs[0]?.id ?? null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectedRunId is a stable Zustand setter
}, [runs, selectedRunId]);
const activeRunId = selectedRun?.id ?? null;
useEffect(() => {
if (!activeRunId) return;
void (async () => {
const activeRun = runs.find((run) => run.id === activeRunId);
if (!activeRun) return;
const result = await window.agentCompany.getRunLog({ companyId: activeRun.companyId, runId: activeRunId });
if (result.ok) {
logBufferRef.current = { ...logBufferRef.current, [activeRunId]: result.data };
incrementLogRevision();
return;
}
setError(result.error.message);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps -- incrementLogRevision and setError are stable Zustand setters
}, [activeRunId, runs]);
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
setLocale(newLocale);
try { await window.agentCompany.updateSettings({ locale: newLocale }); } catch { /* local state already updated */ }
// eslint-disable-next-line react-hooks/exhaustive-deps -- setLocale is a stable Zustand setter
}, []);
const paletteTrapRef = useFocusTrap(commandPaletteOpen);
const paletteQuery = useDeferredValue(commandPaletteSearch.trim().toLowerCase());
const paletteResults = useMemo(() => {
const filteredNavItems = paletteQuery
? navItems.filter((item) => item.label.toLowerCase().includes(paletteQuery) || item.id.toLowerCase().includes(paletteQuery))
: navItems;
const matchedAgents = paletteQuery ? companyAgents.filter((a) => a.name.toLowerCase().includes(paletteQuery) || a.role.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedTasks = paletteQuery ? tasks.filter((t) => t.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedRuns = paletteQuery ? runs.filter((run) => {
const matchingTask = tasks.find((task) => task.id === run.taskId);
return [run.id, run.summary ?? "", run.errorMessage ?? "", matchingTask?.title ?? "", run.connectorId]
.some((value) => value.toLowerCase().includes(paletteQuery));
}).slice(0, 5) : [];
const matchedGoals = paletteQuery ? companyGoals.filter((g) => g.title.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const matchedProjects = paletteQuery ? companyProjects.filter((p) => p.name.toLowerCase().includes(paletteQuery)).slice(0, 5) : [];
const hasEntityResults = matchedAgents.length > 0 || matchedTasks.length > 0 || matchedRuns.length > 0 || matchedGoals.length > 0 || matchedProjects.length > 0;
return { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults };
}, [paletteQuery, companyAgents, companyGoals, companyProjects, runs, tasks, navItems]);
const { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults } = paletteResults;
const paletteActions = useMemo<PaletteAction[]>(() => ([
...filteredNavItems.map((item) => ({ key: `section:${item.id}`, section: item.id })),
...matchedAgents.map((agent) => ({ key: `agent:${agent.id}`, section: "agents" as const, entityId: agent.id })),
...matchedTasks.map((task) => ({ key: `task:${task.id}`, section: "tasks" as const, entityId: task.id })),
...matchedRuns.map((run) => ({ key: `run:${run.id}`, section: "runs" as const, entityId: run.id })),
...matchedGoals.map((goal) => ({ key: `goal:${goal.id}`, section: "goals" as const, entityId: goal.id })),
...matchedProjects.map((project) => ({ key: `project:${project.id}`, section: "projects" as const, entityId: project.id })),
]), [filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects]);
const paletteActionIndex = useMemo(() => new Map(paletteActions.map((action, index) => [action.key, index])), [paletteActions]);
const navigateTo = useCallback((targetSection: SectionId, entityId?: string) => {
setSection(targetSection);
if (entityId) {
if (targetSection === "agents") setSelectedAgentId(entityId);
else if (targetSection === "connectors") setSelectedConnectorId(entityId);
else if (targetSection === "runs") { setSelectedRunId(entityId); setConsoleOpen(true); }
else if (targetSection === "tasks") {
const matchingTask = tasks.find((task) => task.id === entityId) ?? null;
if (matchingTask) { setSelectedTaskId(matchingTask.id); }
else {
const matchingRun = runs.find((run) => run.id === entityId) ?? null;
if (matchingRun) { setSelectedRunId(matchingRun.id); setSelectedTaskId(matchingRun.taskId); setConsoleOpen(true); }
}
}
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
else if (targetSection === "goals") setSelectedGoalId(entityId);
else if (targetSection === "projects") setSelectedProjectId(entityId);
else if (targetSection === "communication") setSelectedCommunicationEntityId(entityId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [runs, tasks]);
useCommandPaletteHotkeys({
commandPaletteOpen,
setCommandPaletteOpen,
setCommandPaletteSearch,
setSelectedPaletteIndex,
paletteActions,
selectedPaletteIndex,
navigateTo,
commandPaletteSearch,
});
const setCompany = useCallback(async (companyId: string) => {
if (companyId === "__new__") { setShowOnboarding(true); return; }
try {
unwrap(await window.agentCompany.setCurrentCompany(companyId));
clearCompanyScopedUiState();
await refresh();
setError(null);
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError and setShowOnboarding are stable Zustand setters
}, [clearCompanyScopedUiState, refresh]);
const deleteCompany = useCallback(async (companyId: string) => {
try {
unwrap(await window.agentCompany.deleteCompany(companyId));
setConfirmDeleteCompanyId(null);
setCompanySwitcherOpen(false);
await refresh();
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [refresh]);
const startRun = useCallback(async (taskId: string) => {
if (!currentCompany) { setError("Select a company before starting a run."); return; }
const result = await window.agentCompany.startTaskRun({ companyId: currentCompany.id, taskId });
if (!result.ok) { setError(result.error.message); return; }
setSelectedRunId(result.data);
setConsoleOpen(true);
await refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [currentCompany, refresh]);
const cancelRun = useCallback(async (runId: string) => {
try {
const run = runs.find((entry) => entry.id === runId);
if (!run) throw new Error("Selected run is no longer available.");
unwrap(await window.agentCompany.cancelRun({ companyId: run.companyId, runId }));
await refresh();
setError(null);
} catch (err) { setError(err instanceof Error ? err.message : String(err)); }
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError is a stable Zustand setter
}, [refresh, runs]);
const handleOnboardingDone = useCallback(async (summary: OnboardingBootstrapResult) => {
setShowOnboarding(false);
setInitialOnboardingLocked(false);
await refresh();
setError(null);
setSection("overview");
if (summary.runId) setSelectedRunId(summary.runId);
if (summary.leadAgentId) setSelectedAgentId(summary.leadAgentId);
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [refresh]);
const openPathWithFeedback = useCallback(async (path: string) => {
try { unwrap(await window.agentCompany.openPath(path)); setError(null); }
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
// eslint-disable-next-line react-hooks/exhaustive-deps -- setError is a stable Zustand setter
}, []);
return {
pageTransition, panelTransition,
locale, t, handleLocaleChange,
sidebarNav, navItems, navBadges,
snapshot, currentCompany,
showOnboarding, initialOnboardingLocked, handleOnboardingDone, setShowOnboarding, setInitialOnboardingLocked, setError,
sidebarCollapsed, setSidebarCollapsed,
companySwitcherOpen, setCompanySwitcherOpen,
confirmDeleteCompanyId, setConfirmDeleteCompanyId,
setCompany, deleteCompany,
section, setSection, error, consoleOpen, setConsoleOpen,
tasks, runs, approvals, pendingApprovals,
companyAgents, companyGoals, companyProjects, companyWorkspaces,
companyCosts, filteredActivity, companySecrets, companyDocuments,
companyKnowledge, companyMeetings, companySprints,
socialAccounts, browserActions, inboxItems, companyMetrics, standupReport,
selectedTask, selectedTaskId, setSelectedTaskId,
selectedConnector, setSelectedConnectorId,
selectedApproval, selectedApprovalId, setSelectedApprovalId,
selectedAgent, selectedAgentId, setSelectedAgentId,
selectedGoal, selectedGoalId, setSelectedGoalId,
selectedProject, selectedProjectId, setSelectedProjectId,
selectedWorkspace, setSelectedWorkspaceId,
selectedCommunicationEntityId, setSelectedCommunicationEntityId,
selectedRun, selectedRunId, setSelectedRunId,
taskViewMode, setTaskViewMode,
logBufferRef,
navigateTo, refresh, startRun, cancelRun, openPathWithFeedback,
commandPaletteOpen, setCommandPaletteOpen,
commandPaletteSearch, setCommandPaletteSearch,
selectedPaletteIndex, setSelectedPaletteIndex,
paletteTrapRef,
filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults,
paletteActions, paletteActionIndex,
};
}
@@ -0,0 +1,65 @@
import { useEffect } from "react";
import { useAppStore } from "../state/store";
import type { PaletteAction } from "./useAppController";
interface CommandPaletteHotkeysOptions {
commandPaletteOpen: boolean;
setCommandPaletteOpen: (open: boolean) => void;
setCommandPaletteSearch: (value: string) => void;
setSelectedPaletteIndex: (index: number) => void;
paletteActions: PaletteAction[];
selectedPaletteIndex: number;
navigateTo: (section: PaletteAction["section"], entityId?: string) => void;
commandPaletteSearch: string;
}
export function useCommandPaletteHotkeys({
commandPaletteOpen,
setCommandPaletteOpen,
setCommandPaletteSearch,
setSelectedPaletteIndex,
paletteActions,
selectedPaletteIndex,
navigateTo,
commandPaletteSearch,
}: CommandPaletteHotkeysOptions) {
useEffect(() => {
function onGlobalKey(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
event.preventDefault();
setCommandPaletteOpen(!commandPaletteOpen);
if (!commandPaletteOpen) {
setCommandPaletteSearch("");
setSelectedPaletteIndex(0);
}
}
}
document.addEventListener("keydown", onGlobalKey);
return () => document.removeEventListener("keydown", onGlobalKey);
}, [commandPaletteOpen, setCommandPaletteOpen, setCommandPaletteSearch, setSelectedPaletteIndex]);
useEffect(() => {
if (!commandPaletteOpen) return;
function onKey(event: KeyboardEvent) {
if (event.key === "Escape") { setCommandPaletteOpen(false); setCommandPaletteSearch(""); setSelectedPaletteIndex(0); return; }
if (paletteActions.length === 0) return;
if (event.key === "ArrowDown") { event.preventDefault(); setSelectedPaletteIndex((useAppStore.getState().selectedPaletteIndex + 1) % paletteActions.length); return; }
if (event.key === "ArrowUp") { event.preventDefault(); setSelectedPaletteIndex((useAppStore.getState().selectedPaletteIndex - 1 + paletteActions.length) % paletteActions.length); return; }
if (event.key !== "Enter") return;
event.preventDefault();
const selectedAction = paletteActions[Math.min(selectedPaletteIndex, paletteActions.length - 1)];
if (!selectedAction) return;
navigateTo(selectedAction.section, selectedAction.entityId);
setCommandPaletteOpen(false);
setCommandPaletteSearch("");
setSelectedPaletteIndex(0);
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [commandPaletteOpen, navigateTo, paletteActions, selectedPaletteIndex, setCommandPaletteOpen, setCommandPaletteSearch, setSelectedPaletteIndex]);
useEffect(() => {
if (!commandPaletteOpen) return;
setSelectedPaletteIndex(0);
}, [commandPaletteOpen, commandPaletteSearch, setSelectedPaletteIndex]);
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Company Context Provider
*
* Provides shared company infrastructure to section components, eliminating
* the need to thread 80+ props through SectionContentView. Components can
* import useCompanyContext() instead of receiving props from the root.
*
* This context holds stable references to company data and actions that
* nearly every section needs: companyId, snapshot, refresh, navigation, etc.
*/
import { createContext, useContext, useMemo, type ReactNode } from "react";
import type { CompanyRecord, ProfileSnapshot, SectionId } from "@shared/types";
export interface CompanyContextValue {
companyId: string | null;
currentCompany: CompanyRecord | null;
snapshot: ProfileSnapshot | null;
refresh: () => void;
navigateTo: (section: SectionId, entityId?: string) => void;
setSection: (section: SectionId) => void;
setError: (error: string | null) => void;
openPath: (targetPath: string) => Promise<void>;
}
const CompanyContext = createContext<CompanyContextValue | null>(null);
interface CompanyContextProviderProps {
companyId: string | null;
currentCompany: CompanyRecord | null;
snapshot: ProfileSnapshot | null;
refresh: () => void;
navigateTo: (section: SectionId, entityId?: string) => void;
setSection: (section: SectionId) => void;
setError: (error: string | null) => void;
openPath: (targetPath: string) => Promise<void>;
children: ReactNode;
}
export function CompanyContextProvider({
companyId, currentCompany, snapshot, refresh,
navigateTo, setSection, setError, openPath, children,
}: CompanyContextProviderProps) {
const value = useMemo<CompanyContextValue>(() => ({
companyId, currentCompany, snapshot, refresh,
navigateTo, setSection, setError, openPath,
}), [companyId, currentCompany, snapshot, refresh, navigateTo, setSection, setError, openPath]);
return <CompanyContext.Provider value={value}>{children}</CompanyContext.Provider>;
}
export function useCompanyContext(): CompanyContextValue {
const ctx = useContext(CompanyContext);
if (!ctx) throw new Error("useCompanyContext must be used within CompanyContextProvider");
return ctx;
}
+79
View File
@@ -0,0 +1,79 @@
import { useMemo } from "react";
import type { CompanyRecord, ProfileSnapshot, SectionId } from "@shared/types";
import { useAppStore } from "../state/store";
export function useCompanyData(
snapshot: ProfileSnapshot | null,
currentCompany: CompanyRecord | null,
) {
const selectedRunId = useAppStore((s) => s.selectedRunId);
const selectedTaskId = useAppStore((s) => s.selectedTaskId);
const selectedApprovalId = useAppStore((s) => s.selectedApprovalId);
const selectedAgentId = useAppStore((s) => s.selectedAgentId);
const selectedGoalId = useAppStore((s) => s.selectedGoalId);
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId);
const tasks = useMemo(() => snapshot?.tasks.filter((task) => !currentCompany || task.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const approvals = useMemo(() => snapshot?.approvals.filter((approval) => !currentCompany || approval.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const pendingApprovals = useMemo(() => approvals.filter((approval) => approval.state === "pending"), [approvals]);
const runs = useMemo(() => snapshot?.runs.filter((run) => !currentCompany || run.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyAgents = useMemo(() => snapshot?.agents.filter((a) => !currentCompany || a.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyGoals = useMemo(() => snapshot?.goals.filter((g) => !currentCompany || g.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyProjects = useMemo(() => snapshot?.projects.filter((p) => !currentCompany || p.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyWorkspaces = useMemo(() => snapshot?.workspaces.filter((workspace) => !currentCompany || workspace.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyCosts = useMemo(() => snapshot?.costs.filter((entry) => !currentCompany || entry.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const filteredActivity = useMemo(() => snapshot?.activity.filter((entry) => !currentCompany || entry.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companySecrets = useMemo(() => snapshot?.secrets.filter((secret) => !currentCompany || secret.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyDocuments = useMemo(() => snapshot?.documents?.filter((d) => !currentCompany || d.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyKnowledge = useMemo(() => snapshot?.knowledgeBase?.filter((k) => !currentCompany || k.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companyMeetings = useMemo(() => snapshot?.meetings?.filter((m) => !currentCompany || m.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const companySprints = useMemo(() => snapshot?.sprints?.filter((s) => !currentCompany || s.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]);
const selectedTask = selectedTaskId ? (tasks.find((task) => task.id === selectedTaskId) ?? null) : null;
const selectedApproval = selectedApprovalId ? (approvals.find((approval) => approval.id === selectedApprovalId) ?? null) : null;
const selectedRun = runs.find((run) => run.id === selectedRunId) ?? runs[0] ?? null;
const selectedAgent = companyAgents.find((a) => a.id === selectedAgentId) ?? null;
const selectedGoal = companyGoals.find((g) => g.id === selectedGoalId) ?? null;
const selectedProject = companyProjects.find((p) => p.id === selectedProjectId) ?? null;
const selectedWorkspace = companyWorkspaces.find((workspace) => workspace.id === selectedWorkspaceId) ?? companyWorkspaces[0] ?? null;
const navBadgesSource = useAppStore((s) => s.badges);
const navBadges = useMemo<Partial<Record<SectionId, { count: number; tone: "default" | "warn" | "danger" }>>>(() => navBadgesSource ? {
runs:
navBadgesSource.failedRuns > 0
? { count: navBadgesSource.failedRuns, tone: "danger" }
: navBadgesSource.activeRuns > 0
? { count: navBadgesSource.activeRuns, tone: "default" }
: undefined,
tasks: navBadgesSource.todoTasks > 0 ? { count: navBadgesSource.todoTasks, tone: "default" } : undefined,
approvals: navBadgesSource.pendingApprovals > 0 ? { count: navBadgesSource.pendingApprovals, tone: "warn" } : undefined,
communication: navBadgesSource.unreadMessages > 0 ? { count: navBadgesSource.unreadMessages, tone: "warn" } : undefined,
} : {}, [navBadgesSource]);
return {
tasks,
approvals,
pendingApprovals,
runs,
companyAgents,
companyGoals,
companyProjects,
companyWorkspaces,
companyCosts,
filteredActivity,
companySecrets,
companyDocuments,
companyKnowledge,
companyMeetings,
companySprints,
selectedTask,
selectedApproval,
selectedRun,
selectedAgent,
selectedGoal,
selectedProject,
selectedWorkspace,
navBadges,
};
}
+85
View File
@@ -0,0 +1,85 @@
import { useCallback, useEffect } from "react";
import type { ProfileSnapshot, SectionId } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { useAppStore } from "../state/store";
export function useDataLoader(
snapshot: ProfileSnapshot | null,
section: SectionId,
selectedConnectorId: string | null,
selectedRunId: string | null,
setSnapshot: (snapshot: ProfileSnapshot) => void,
setError: (error: string | null) => void,
setInitialOnboardingLocked: (locked: boolean) => void,
setSelectedConnectorId: (id: string | null) => void,
setSelectedRunId: (id: string | null) => void,
setBadges: (badges: Parameters<ReturnType<typeof useAppStore.getState>["setBadges"]>[0]) => void,
setInboxItems: (items: Parameters<ReturnType<typeof useAppStore.getState>["setInboxItems"]>[0]) => void,
setSocialAccounts: (accounts: Parameters<ReturnType<typeof useAppStore.getState>["setSocialAccounts"]>[0]) => void,
setBrowserActions: (actions: Parameters<ReturnType<typeof useAppStore.getState>["setBrowserActions"]>[0]) => void,
setCompanyMetrics: (metrics: Parameters<ReturnType<typeof useAppStore.getState>["setCompanyMetrics"]>[0]) => void,
setStandupReport: (report: Parameters<ReturnType<typeof useAppStore.getState>["setStandupReport"]>[0]) => void,
) {
const refresh = useCallback(async () => {
try {
const next = unwrap<ProfileSnapshot>(await window.agentCompany.loadSnapshot());
setSnapshot(next);
if (next.companies.length === 0) {
setInitialOnboardingLocked(true);
}
if (!selectedConnectorId && next.connectors[0]) setSelectedConnectorId(next.connectors[0].id);
if (!selectedRunId && next.runs[0]) setSelectedRunId(next.runs[0].id);
if (next.currentCompanyId) {
const badgeResult = await window.agentCompany.getSidebarBadges(next.currentCompanyId);
if (badgeResult.ok) setBadges(badgeResult.data);
const inboxResult = await window.agentCompany.getInbox(next.currentCompanyId);
if (inboxResult.ok) setInboxItems(inboxResult.data);
setSocialAccounts(next.socialAccounts?.filter(a => a.companyId === next.currentCompanyId) ?? []);
const browserActionResult = await window.agentCompany.listBrowserActions({ companyId: next.currentCompanyId, limit: 50 });
if (browserActionResult.ok) {
setBrowserActions(browserActionResult.data);
}
}
setError(null);
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : String(nextError));
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [selectedConnectorId, selectedRunId]);
// Section-based data loading
useEffect(() => {
if (!snapshot?.currentCompanyId) return;
const companyId = snapshot.currentCompanyId;
let cancelled = false;
if (section === "inbox") {
void window.agentCompany.getInbox(companyId).then((result) => {
if (!cancelled && result.ok) setInboxItems(result.data);
});
}
if (section === "performance") {
void window.agentCompany.getCompanyMetrics({ companyId, days: 30 }).then((result) => {
if (!cancelled && result.ok) setCompanyMetrics(result.data);
});
}
if (section === "standup") {
void window.agentCompany.getStandupReport(companyId).then((result) => {
if (!cancelled && result.ok) setStandupReport(result.data);
});
}
if (section === "social") {
setSocialAccounts(snapshot.socialAccounts?.filter((a) => a.companyId === companyId) ?? []);
}
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters are stable references
}, [section, snapshot]);
return { refresh };
}
+152
View File
@@ -0,0 +1,152 @@
import { useEffect, type MutableRefObject } from "react";
import type { DesktopEvent } from "@shared/types";
import { createDomainEventBatcher, type GranularEventEntry } from "../lib/ipc-batcher";
import { useAppStore } from "../state/store";
export function useDesktopEvents(
refreshRef: MutableRefObject<() => Promise<void>>,
logBufferRef: MutableRefObject<Record<string, string>>,
incrementLogRevision: () => void,
) {
useEffect(() => {
void refreshRef.current();
const store = useAppStore;
async function handleGranularBatch(events: GranularEventEntry[]) {
const companyId = store.getState().currentCompanyId;
if (!companyId) {
void refreshRef.current();
return;
}
let needsFullRefresh = false;
for (const entry of events) {
// run-updated:completed touches too many entity types -- full refresh
if (entry.type === "run-updated" && entry.action === "completed") {
needsFullRefresh = true;
break;
}
try {
switch (entry.type) {
case "task-updated": {
if (entry.action === "deleted") {
store.getState().removeTask(entry.entityId);
} else {
const result = await window.agentCompany.getTask({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchTask(result.data);
} else {
store.getState().removeTask(entry.entityId);
}
}
break;
}
case "run-updated": {
if (entry.action === "deleted") {
store.getState().removeRun(entry.entityId);
} else {
const result = await window.agentCompany.getRun({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchRun(result.data);
} else {
store.getState().removeRun(entry.entityId);
}
}
break;
}
case "approval-updated": {
if (entry.action === "deleted") {
store.getState().removeApproval(entry.entityId);
} else {
const result = await window.agentCompany.getApproval({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchApproval(result.data);
} else {
store.getState().removeApproval(entry.entityId);
}
}
break;
}
case "agent-updated": {
const result = await window.agentCompany.getAgent({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchAgent(result.data);
} else {
needsFullRefresh = true;
}
break;
}
case "budget-changed": {
// Budget is a computed aggregate -- full refresh is needed
needsFullRefresh = true;
break;
}
default: {
// Unknown granular event type -- fall back (per D-05 / PERF-03)
needsFullRefresh = true;
break;
}
}
} catch {
// IPC error during entity fetch -- fall back to full refresh (per pitfall 1)
needsFullRefresh = true;
break;
}
if (needsFullRefresh) break;
}
if (needsFullRefresh) {
void refreshRef.current();
} else {
// Refresh sidebar badges after granular patches (per pitfall 3)
void store.getState().refreshBadges();
}
}
const batcher = createDomainEventBatcher(
() => { void refreshRef.current(); },
(events) => { void handleGranularBatch(events); },
50,
);
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
if (event.type === "run-log") {
const MAX_LOG_BYTES_PER_RUN = 512_000;
const MAX_TRACKED_RUNS = 50;
const existing = logBufferRef.current[event.runId] ?? "";
const updated = existing.length < MAX_LOG_BYTES_PER_RUN
? existing + event.chunk
: existing.slice(-MAX_LOG_BYTES_PER_RUN / 2) + event.chunk;
logBufferRef.current[event.runId] = updated;
const runIds = Object.keys(logBufferRef.current);
if (runIds.length > MAX_TRACKED_RUNS) {
for (const oldId of runIds.slice(0, runIds.length - MAX_TRACKED_RUNS)) {
delete logBufferRef.current[oldId];
}
}
incrementLogRevision();
return;
}
if (event.type === "shortcut") {
if (event.action === "toggle-command-palette") store.getState().toggleCommandPalette();
if (event.action === "toggle-console") store.getState().toggleConsole();
if (event.action === "refresh") void refreshRef.current();
return;
}
if (event.type === "notification-click") {
store.getState().navigateTo(event.section, event.entityId);
return;
}
// All data-mutating events go through the enhanced batcher
batcher.push(event);
});
return () => {
unsubscribe();
batcher.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- refreshRef and logBufferRef are stable refs; incrementLogRevision is a stable Zustand setter
}, []);
}