refactor(renderer): dismantle useAppController 47-prop god-hook

Sections now subscribe directly to the Zustand store via useSectionContext
(shared data derivation) and useAppStore fine-grained selectors. Delete
useAppController, useExecutionController, useOrganizationController, and
useCommunicationController; the three domain forwarders held no logic
beyond re-exporting store slices.

SectionContentView becomes a 50-line layout dispatcher taking only
{section, error, pageTransition}; cross-domain run actions (startRun /
cancelRun) move into App.tsx and are exposed to sections through a new
AppActionsContext alongside refresh / setError / openPathWithFeedback.
App.tsx now calls useUIController + useCompanyController directly and
composes palette + navigation plumbing itself.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 22:12:51 -04:00
1 parent de77f5f38e
commit e2253b92fa
12 files changed
+339 -897

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
import { createContext, useContext } from "react";
export interface AppActions {
refresh: () => Promise<void>;
setError: (error: string | null) => void;
openPathWithFeedback: (path: string) => Promise<void>;
startRun: (taskId: string) => Promise<void>;
cancelRun: (runId: string) => Promise<void>;
}
export const AppActionsContext = createContext<AppActions | null>(null);
export function useAppActions(): AppActions {
const value = useContext(AppActionsContext);
if (!value) throw new Error("useAppActions must be used inside <AppActionsContext.Provider>");
return value;
}
-209
View File
@@ -1,209 +0,0 @@
import { useCallback, useMemo } from "react";
import { useAppStore } from "../state/store";
import { useUIController } from "./useUIController";
import { useCompanyController } from "./useCompanyController";
import { useExecutionController } from "./useExecutionController";
import { useOrganizationController } from "./useOrganizationController";
import { useCommunicationController } from "./useCommunicationController";
import { useCommandPaletteHotkeys } from "./useCommandPaletteHotkeys";
import { unwrap } from "../lib/desktop";
// Re-export types consumed by other modules
export type { NavItem, PaletteAction } from "./useUIController";
export function useAppController() {
const ui = useUIController();
const company = useCompanyController();
const execution = useExecutionController();
const organization = useOrganizationController();
const communication = useCommunicationController();
const error = useAppStore((s) => s.error);
const setError = useAppStore((s) => s.setError);
const inboxItems = useAppStore((s) => s.inboxItems);
const companyMetrics = useAppStore((s) => s.companyMetrics);
const standupReport = useAppStore((s) => s.standupReport);
const socialAccounts = useAppStore((s) => s.socialAccounts);
const browserActions = useAppStore((s) => s.browserActions);
// --- Cross-domain actions composed here ---
const startRun = useCallback(async (taskId: string) => {
if (!company.currentCompany) { setError("Select a company before starting a run."); return; }
const result = await window.agentCompany.startTaskRun({ companyId: company.currentCompany.id, taskId });
if (!result.ok) { setError(result.error.message); return; }
execution.setSelectedRunId(result.data);
ui.setConsoleOpen(true);
await company.refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Zustand setters and company.refresh are stable
}, [company.currentCompany, company.refresh]);
const cancelRun = useCallback(async (runId: string) => {
try {
const run = company.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 company.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
}, [company.refresh, company.runs]);
// --- Command palette results (needs entity lists from company domain) ---
const { filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects, hasEntityResults } =
useMemo(() => ui.buildPaletteResults(
ui.navItems,
company.companyAgents,
company.tasks,
company.runs,
company.companyGoals,
company.companyProjects,
), [
ui.navItems,
ui.paletteQuery,
company.companyAgents,
company.tasks,
company.runs,
company.companyGoals,
company.companyProjects,
]);
const paletteActions = useMemo(
() => ui.buildPaletteActions(filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects),
[filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects],
);
const paletteActionIndex = useMemo(
() => new Map(paletteActions.map((action, index) => [action.key, index])),
[paletteActions],
);
// navigateTo needs entity lists for task/run cross-lookup
const navigateTo = useCallback((targetSection: Parameters<typeof ui.navigateTo>[0], entityId?: string) => {
ui.navigateTo(targetSection, entityId, company.runs, company.tasks);
}, [ui.navigateTo, company.runs, company.tasks]);
// Wire command palette hotkeys
useCommandPaletteHotkeys({
commandPaletteOpen: ui.commandPaletteOpen,
setCommandPaletteOpen: ui.setCommandPaletteOpen,
setCommandPaletteSearch: ui.setCommandPaletteSearch,
setSelectedPaletteIndex: ui.setSelectedPaletteIndex,
paletteActions,
selectedPaletteIndex: ui.selectedPaletteIndex,
navigateTo,
commandPaletteSearch: ui.commandPaletteSearch,
});
return {
// UI / navigation
pageTransition: ui.pageTransition,
panelTransition: ui.panelTransition,
locale: ui.locale,
t: ui.t,
handleLocaleChange: ui.handleLocaleChange,
sidebarNav: ui.sidebarNav,
navItems: ui.navItems,
section: ui.section,
setSection: ui.setSection,
consoleOpen: ui.consoleOpen,
setConsoleOpen: ui.setConsoleOpen,
sidebarCollapsed: ui.sidebarCollapsed,
setSidebarCollapsed: ui.setSidebarCollapsed,
companySwitcherOpen: ui.companySwitcherOpen,
setCompanySwitcherOpen: ui.setCompanySwitcherOpen,
confirmDeleteCompanyId: ui.confirmDeleteCompanyId,
setConfirmDeleteCompanyId: ui.setConfirmDeleteCompanyId,
taskViewMode: ui.taskViewMode,
setTaskViewMode: ui.setTaskViewMode,
showOnboarding: ui.showOnboarding,
setShowOnboarding: ui.setShowOnboarding,
initialOnboardingLocked: ui.initialOnboardingLocked,
setInitialOnboardingLocked: ui.setInitialOnboardingLocked,
commandPaletteOpen: ui.commandPaletteOpen,
setCommandPaletteOpen: ui.setCommandPaletteOpen,
commandPaletteSearch: ui.commandPaletteSearch,
setCommandPaletteSearch: ui.setCommandPaletteSearch,
selectedPaletteIndex: ui.selectedPaletteIndex,
setSelectedPaletteIndex: ui.setSelectedPaletteIndex,
paletteTrapRef: ui.paletteTrapRef,
filteredNavItems,
matchedAgents,
matchedTasks,
matchedRuns,
matchedGoals,
matchedProjects,
hasEntityResults,
paletteActions,
paletteActionIndex,
navigateTo,
// Company / core
snapshot: company.snapshot,
currentCompany: company.currentCompany,
selectedConnector: company.selectedConnector,
logBufferRef: company.logBufferRef,
refresh: company.refresh,
setCompany: company.setCompany,
deleteCompany: company.deleteCompany,
handleOnboardingDone: company.handleOnboardingDone,
openPathWithFeedback: company.openPathWithFeedback,
tasks: company.tasks,
runs: company.runs,
approvals: company.approvals,
pendingApprovals: company.pendingApprovals,
companyAgents: company.companyAgents,
companyGoals: company.companyGoals,
companyProjects: company.companyProjects,
companyWorkspaces: company.companyWorkspaces,
companyCosts: company.companyCosts,
filteredActivity: company.filteredActivity,
companySecrets: company.companySecrets,
companyDocuments: company.companyDocuments,
companyKnowledge: company.companyKnowledge,
companyMeetings: company.companyMeetings,
companySprints: company.companySprints,
selectedTask: company.selectedTask,
selectedApproval: company.selectedApproval,
selectedRun: company.selectedRun,
selectedAgent: company.selectedAgent,
selectedGoal: company.selectedGoal,
selectedProject: company.selectedProject,
selectedWorkspace: company.selectedWorkspace,
navBadges: company.navBadges,
// Execution
selectedTaskId: execution.selectedTaskId,
setSelectedTaskId: execution.setSelectedTaskId,
setSelectedConnectorId: execution.setSelectedConnectorId,
selectedApprovalId: execution.selectedApprovalId,
setSelectedApprovalId: execution.setSelectedApprovalId,
selectedRunId: execution.selectedRunId,
setSelectedRunId: execution.setSelectedRunId,
startRun,
cancelRun,
// Organization
selectedAgentId: organization.selectedAgentId,
setSelectedAgentId: organization.setSelectedAgentId,
selectedGoalId: organization.selectedGoalId,
setSelectedGoalId: organization.setSelectedGoalId,
selectedProjectId: organization.selectedProjectId,
setSelectedProjectId: organization.setSelectedProjectId,
setSelectedWorkspaceId: organization.setSelectedWorkspaceId,
// Communication
selectedCommunicationEntityId: communication.selectedCommunicationEntityId,
setSelectedCommunicationEntityId: communication.setSelectedCommunicationEntityId,
// Shared cross-domain state
error,
setError,
inboxItems,
companyMetrics,
standupReport,
socialAccounts,
browserActions,
};
}
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useAppStore } from "../state/store";
import type { PaletteAction } from "./useAppController";
import type { PaletteAction } from "./useUIController";
interface CommandPaletteHotkeysOptions {
commandPaletteOpen: boolean;
@@ -1,37 +0,0 @@
import { useAppStore } from "../state/store";
/**
* Fine-grained subscriptions for communication domain:
* messages, channels, threads, search state.
*/
export function useCommunicationController() {
const selectedCommunicationEntityId = useAppStore((s) => s.selectedCommunicationEntityId);
const setSelectedCommunicationEntityId = useAppStore((s) => s.setSelectedCommunicationEntityId);
const searchQuery = useAppStore((s) => s.searchQuery);
const setSearchQuery = useAppStore((s) => s.setSearchQuery);
const searchScope = useAppStore((s) => s.searchScope);
const setSearchScope = useAppStore((s) => s.setSearchScope);
const searchResults = useAppStore((s) => s.searchResults);
const setSearchResults = useAppStore((s) => s.setSearchResults);
const searchLoading = useAppStore((s) => s.searchLoading);
const setSearchLoading = useAppStore((s) => s.setSearchLoading);
const clearSearch = useAppStore((s) => s.clearSearch);
const activeThreadId = useAppStore((s) => s.activeThreadId);
const setActiveThreadId = useAppStore((s) => s.setActiveThreadId);
return {
selectedCommunicationEntityId,
setSelectedCommunicationEntityId,
searchQuery,
setSearchQuery,
searchScope,
setSearchScope,
searchResults,
setSearchResults,
searchLoading,
setSearchLoading,
clearSearch,
activeThreadId,
setActiveThreadId,
};
}
@@ -1,30 +0,0 @@
import { useAppStore } from "../state/store";
/**
* Fine-grained subscriptions for execution domain:
* runs, approvals, costs, budgets, selected run/approval/task/connector.
*
* Cross-domain actions (startRun, cancelRun) are composed in useAppController
* because they need refresh() from useCompanyController.
*/
export function useExecutionController() {
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);
return {
selectedTaskId,
setSelectedTaskId,
selectedConnectorId,
setSelectedConnectorId,
selectedApprovalId,
setSelectedApprovalId,
selectedRunId,
setSelectedRunId,
};
}
@@ -1,27 +0,0 @@
import { useAppStore } from "../state/store";
/**
* Fine-grained subscriptions for organization domain:
* goals, projects, sprints, departments, workspaces, agents.
*/
export function useOrganizationController() {
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);
return {
selectedAgentId,
setSelectedAgentId,
selectedGoalId,
setSelectedGoalId,
selectedProjectId,
setSelectedProjectId,
selectedWorkspaceId,
setSelectedWorkspaceId,
};
}
+40
View File
@@ -0,0 +1,40 @@
import { useMemo } from "react";
import type { CompanyRecord } from "@shared/types";
import { useAppStore } from "../state/store";
import { useCompanyData } from "./useCompanyData";
/**
* Shared data context for section views. Subscribes to the Zustand store
* for snapshot + current company, derives per-company collections and
* currently-selected entities via useCompanyData.
*
* Sections call this hook directly instead of receiving 30+ props through
* SectionContentView.
*/
export function useSectionContext() {
const snapshot = useAppStore((s) => s.snapshot);
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 selectedConnectorId = useAppStore((s) => s.selectedConnectorId);
const selectedConnector =
snapshot?.connectors.find((connector) => connector.id === selectedConnectorId) ??
snapshot?.connectors[0] ??
null;
return {
snapshot,
currentCompany,
selectedConnector,
...companyData,
};
}