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:
1 parent
de77f5f38e
commit
e2253b92fa
12 files changed
+339
-897
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user