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
+161
-130
@@ -1,3 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { CircleNotch, TerminalWindow } from "@phosphor-icons/react";
|
||||
import { LocaleContext, TranslationContext } from "./i18n";
|
||||
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
||||
@@ -7,23 +8,107 @@ import { SidebarView } from "./views/SidebarView";
|
||||
import { SectionContentView } from "./views/SectionContentView";
|
||||
import { ConsoleView } from "./views/ConsoleView";
|
||||
import { CommandPaletteView } from "./views/CommandPaletteView";
|
||||
import { useAppController } from "./hooks/useAppController";
|
||||
import { useAppStore } from "./state/store";
|
||||
import { useUIController } from "./hooks/useUIController";
|
||||
import { useCompanyController } from "./hooks/useCompanyController";
|
||||
import { useCommandPaletteHotkeys } from "./hooks/useCommandPaletteHotkeys";
|
||||
import { AppActionsContext, type AppActions } from "./hooks/useAppActions";
|
||||
import { unwrap } from "./lib/desktop";
|
||||
|
||||
export function App() {
|
||||
const c = useAppController();
|
||||
const ui = useUIController();
|
||||
const company = useCompanyController();
|
||||
|
||||
const error = useAppStore((s) => s.error);
|
||||
const setError = useAppStore((s) => s.setError);
|
||||
const setSelectedRunId = useAppStore((s) => s.setSelectedRunId);
|
||||
const setConsoleOpen = useAppStore((s) => s.setConsoleOpen);
|
||||
|
||||
const showCustomTitleBar = window.agentCompany.platform !== "darwin";
|
||||
const isMac = !showCustomTitleBar;
|
||||
|
||||
if (!c.snapshot) {
|
||||
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; }
|
||||
setSelectedRunId(result.data);
|
||||
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]);
|
||||
|
||||
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),
|
||||
[ui, filteredNavItems, matchedAgents, matchedTasks, matchedRuns, matchedGoals, matchedProjects],
|
||||
);
|
||||
|
||||
const paletteActionIndex = useMemo(
|
||||
() => new Map(paletteActions.map((action, index) => [action.key, index])),
|
||||
[paletteActions],
|
||||
);
|
||||
|
||||
const navigateTo = useCallback((targetSection: Parameters<typeof ui.navigateTo>[0], entityId?: string) => {
|
||||
ui.navigateTo(targetSection, entityId, company.runs, company.tasks);
|
||||
}, [ui, company.runs, company.tasks]);
|
||||
|
||||
useCommandPaletteHotkeys({
|
||||
commandPaletteOpen: ui.commandPaletteOpen,
|
||||
setCommandPaletteOpen: ui.setCommandPaletteOpen,
|
||||
setCommandPaletteSearch: ui.setCommandPaletteSearch,
|
||||
setSelectedPaletteIndex: ui.setSelectedPaletteIndex,
|
||||
paletteActions,
|
||||
selectedPaletteIndex: ui.selectedPaletteIndex,
|
||||
navigateTo,
|
||||
commandPaletteSearch: ui.commandPaletteSearch,
|
||||
});
|
||||
|
||||
const appActions = useMemo<AppActions>(() => ({
|
||||
refresh: company.refresh,
|
||||
setError,
|
||||
openPathWithFeedback: company.openPathWithFeedback,
|
||||
startRun,
|
||||
cancelRun,
|
||||
}), [company.refresh, company.openPathWithFeedback, startRun, cancelRun, setError]);
|
||||
|
||||
if (!company.snapshot) {
|
||||
return (
|
||||
<LocaleContext.Provider value={c.locale}>
|
||||
<TranslationContext.Provider value={c.t}>
|
||||
<LocaleContext.Provider value={ui.locale}>
|
||||
<TranslationContext.Provider value={ui.t}>
|
||||
<div className="flex h-[100dvh] flex-col overflow-hidden">
|
||||
{showCustomTitleBar && <TitleBar />}
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3">
|
||||
<CircleNotch size={24} className="animate-spin text-primary" />
|
||||
<span className="text-[13px] text-muted-foreground">{c.t("app.loading")}</span>
|
||||
<span className="text-[13px] text-muted-foreground">{ui.t("app.loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TranslationContext.Provider>
|
||||
@@ -31,25 +116,25 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
if (c.snapshot.companies.length === 0 || c.showOnboarding || c.initialOnboardingLocked) {
|
||||
if (company.snapshot.companies.length === 0 || ui.showOnboarding || ui.initialOnboardingLocked) {
|
||||
return (
|
||||
<LocaleContext.Provider value={c.locale}>
|
||||
<TranslationContext.Provider value={c.t}>
|
||||
<LocaleContext.Provider value={ui.locale}>
|
||||
<TranslationContext.Provider value={ui.t}>
|
||||
<div className="flex h-[100dvh] flex-col overflow-hidden">
|
||||
{showCustomTitleBar && <TitleBar />}
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<CompanyOnboarding
|
||||
connectors={c.snapshot.connectors}
|
||||
onDone={c.handleOnboardingDone}
|
||||
canCancel={c.snapshot.companies.length > 0 && !c.initialOnboardingLocked}
|
||||
connectors={company.snapshot.connectors}
|
||||
onDone={company.handleOnboardingDone}
|
||||
canCancel={company.snapshot.companies.length > 0 && !ui.initialOnboardingLocked}
|
||||
onCancel={() => {
|
||||
c.setShowOnboarding(false);
|
||||
c.setInitialOnboardingLocked(false);
|
||||
c.setError(null);
|
||||
ui.setShowOnboarding(false);
|
||||
ui.setInitialOnboardingLocked(false);
|
||||
setError(null);
|
||||
}}
|
||||
currentLocale={c.locale}
|
||||
onLocaleChange={c.handleLocaleChange}
|
||||
isFirstLaunch={c.snapshot.companies.length === 0}
|
||||
currentLocale={ui.locale}
|
||||
onLocaleChange={ui.handleLocaleChange}
|
||||
isFirstLaunch={company.snapshot.companies.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,154 +144,100 @@ export function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={c.locale}>
|
||||
<TranslationContext.Provider value={c.t}>
|
||||
<LocaleContext.Provider value={ui.locale}>
|
||||
<TranslationContext.Provider value={ui.t}>
|
||||
<AppActionsContext.Provider value={appActions}>
|
||||
<div className="flex h-[100dvh] flex-col overflow-hidden bg-background">
|
||||
{showCustomTitleBar && <TitleBar />}
|
||||
<div className={`grid min-h-0 flex-1 overflow-hidden transition-[grid-template-columns] duration-200 ${c.sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`} style={{ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)" }}>
|
||||
<div className={`grid min-h-0 flex-1 overflow-hidden transition-[grid-template-columns] duration-200 ${ui.sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`} style={{ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)" }}>
|
||||
|
||||
<SidebarView
|
||||
isMac={isMac}
|
||||
sidebarCollapsed={c.sidebarCollapsed}
|
||||
setSidebarCollapsed={c.setSidebarCollapsed}
|
||||
section={c.section}
|
||||
setSection={c.setSection}
|
||||
sidebarNav={c.sidebarNav}
|
||||
navBadges={c.navBadges}
|
||||
companySwitcherOpen={c.companySwitcherOpen}
|
||||
setCompanySwitcherOpen={c.setCompanySwitcherOpen}
|
||||
confirmDeleteCompanyId={c.confirmDeleteCompanyId}
|
||||
setConfirmDeleteCompanyId={c.setConfirmDeleteCompanyId}
|
||||
currentCompany={c.currentCompany}
|
||||
snapshot={c.snapshot}
|
||||
setCompany={c.setCompany}
|
||||
deleteCompany={c.deleteCompany}
|
||||
setCommandPaletteOpen={c.setCommandPaletteOpen}
|
||||
t={c.t}
|
||||
sidebarCollapsed={ui.sidebarCollapsed}
|
||||
setSidebarCollapsed={ui.setSidebarCollapsed}
|
||||
section={ui.section}
|
||||
setSection={ui.setSection}
|
||||
sidebarNav={ui.sidebarNav}
|
||||
navBadges={company.navBadges}
|
||||
companySwitcherOpen={ui.companySwitcherOpen}
|
||||
setCompanySwitcherOpen={ui.setCompanySwitcherOpen}
|
||||
confirmDeleteCompanyId={ui.confirmDeleteCompanyId}
|
||||
setConfirmDeleteCompanyId={ui.setConfirmDeleteCompanyId}
|
||||
currentCompany={company.currentCompany}
|
||||
snapshot={company.snapshot}
|
||||
setCompany={company.setCompany}
|
||||
deleteCompany={company.deleteCompany}
|
||||
setCommandPaletteOpen={ui.setCommandPaletteOpen}
|
||||
t={ui.t}
|
||||
/>
|
||||
|
||||
<main aria-label="Content" className="flex min-w-0 flex-col overflow-hidden">
|
||||
<header className="sidebar-drag shrink-0 border-b border-border bg-background">
|
||||
<div className="mx-auto flex w-full max-w-[1040px] items-start justify-between gap-4 px-6 pb-5 pt-6 lg:gap-6 lg:px-10 lg:pt-8">
|
||||
<h1 className="text-[28px] font-bold tracking-[-0.02em] text-foreground">
|
||||
{c.navItems.find((item) => item.id === c.section)?.label ?? c.t(`nav.${c.section}`)}
|
||||
{ui.navItems.find((item) => item.id === ui.section)?.label ?? ui.t(`nav.${ui.section}`)}
|
||||
</h1>
|
||||
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={c.consoleOpen ? "Hide console" : "Show console"}
|
||||
title={c.consoleOpen ? "Hide console" : "Show console"}
|
||||
aria-label={ui.consoleOpen ? "Hide console" : "Show console"}
|
||||
title={ui.consoleOpen ? "Hide console" : "Show console"}
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-muted-foreground transition hover:text-foreground"
|
||||
onClick={() => c.setConsoleOpen(!c.consoleOpen)}
|
||||
onClick={() => ui.setConsoleOpen(!ui.consoleOpen)}
|
||||
>
|
||||
<TerminalWindow size={16} />
|
||||
<span className="hidden lg:inline">{c.consoleOpen ? c.t("app.hideConsole") : c.t("app.showConsole")}</span>
|
||||
<span className="hidden lg:inline">{ui.consoleOpen ? ui.t("app.hideConsole") : ui.t("app.showConsole")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<SectionContentView
|
||||
section={c.section}
|
||||
error={c.error}
|
||||
snapshot={c.snapshot}
|
||||
currentCompany={c.currentCompany}
|
||||
locale={c.locale}
|
||||
tasks={c.tasks}
|
||||
runs={c.runs}
|
||||
approvals={c.approvals}
|
||||
pendingApprovals={c.pendingApprovals}
|
||||
companyAgents={c.companyAgents}
|
||||
companyGoals={c.companyGoals}
|
||||
companyProjects={c.companyProjects}
|
||||
companyWorkspaces={c.companyWorkspaces}
|
||||
companyCosts={c.companyCosts}
|
||||
filteredActivity={c.filteredActivity}
|
||||
companySecrets={c.companySecrets}
|
||||
companyDocuments={c.companyDocuments}
|
||||
companyKnowledge={c.companyKnowledge}
|
||||
companyMeetings={c.companyMeetings}
|
||||
companySprints={c.companySprints}
|
||||
socialAccounts={c.socialAccounts}
|
||||
browserActions={c.browserActions}
|
||||
inboxItems={c.inboxItems}
|
||||
companyMetrics={c.companyMetrics}
|
||||
standupReport={c.standupReport}
|
||||
selectedTask={c.selectedTask}
|
||||
selectedTaskId={c.selectedTaskId}
|
||||
setSelectedTaskId={c.setSelectedTaskId}
|
||||
selectedConnector={c.selectedConnector}
|
||||
setSelectedConnectorId={c.setSelectedConnectorId}
|
||||
selectedApproval={c.selectedApproval}
|
||||
selectedApprovalId={c.selectedApprovalId}
|
||||
setSelectedApprovalId={c.setSelectedApprovalId}
|
||||
selectedAgent={c.selectedAgent}
|
||||
selectedAgentId={c.selectedAgentId}
|
||||
setSelectedAgentId={c.setSelectedAgentId}
|
||||
selectedGoal={c.selectedGoal}
|
||||
selectedGoalId={c.selectedGoalId}
|
||||
setSelectedGoalId={c.setSelectedGoalId}
|
||||
selectedProject={c.selectedProject}
|
||||
selectedProjectId={c.selectedProjectId}
|
||||
setSelectedProjectId={c.setSelectedProjectId}
|
||||
selectedWorkspace={c.selectedWorkspace}
|
||||
setSelectedWorkspaceId={c.setSelectedWorkspaceId}
|
||||
selectedCommunicationEntityId={c.selectedCommunicationEntityId}
|
||||
setSelectedCommunicationEntityId={c.setSelectedCommunicationEntityId}
|
||||
taskViewMode={c.taskViewMode}
|
||||
setTaskViewMode={c.setTaskViewMode}
|
||||
selectedRunId={c.selectedRunId}
|
||||
navigateTo={c.navigateTo}
|
||||
setSection={c.setSection}
|
||||
setConsoleOpen={c.setConsoleOpen}
|
||||
setSelectedRunId={c.setSelectedRunId}
|
||||
refresh={c.refresh}
|
||||
startRun={c.startRun}
|
||||
cancelRun={c.cancelRun}
|
||||
openPathWithFeedback={c.openPathWithFeedback}
|
||||
setError={c.setError}
|
||||
pageTransition={c.pageTransition}
|
||||
section={ui.section}
|
||||
error={error}
|
||||
pageTransition={ui.pageTransition}
|
||||
/>
|
||||
|
||||
<ConsoleView
|
||||
consoleOpen={c.consoleOpen}
|
||||
setConsoleOpen={c.setConsoleOpen}
|
||||
selectedRun={c.selectedRun}
|
||||
setSelectedRunId={c.setSelectedRunId}
|
||||
runs={c.runs}
|
||||
logBufferRef={c.logBufferRef}
|
||||
openPathWithFeedback={c.openPathWithFeedback}
|
||||
panelTransition={c.panelTransition}
|
||||
t={c.t}
|
||||
consoleOpen={ui.consoleOpen}
|
||||
setConsoleOpen={ui.setConsoleOpen}
|
||||
selectedRun={company.selectedRun}
|
||||
setSelectedRunId={setSelectedRunId}
|
||||
runs={company.runs}
|
||||
logBufferRef={company.logBufferRef}
|
||||
openPathWithFeedback={company.openPathWithFeedback}
|
||||
panelTransition={ui.panelTransition}
|
||||
t={ui.t}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<CommandPaletteView
|
||||
commandPaletteOpen={c.commandPaletteOpen}
|
||||
setCommandPaletteOpen={c.setCommandPaletteOpen}
|
||||
commandPaletteSearch={c.commandPaletteSearch}
|
||||
setCommandPaletteSearch={c.setCommandPaletteSearch}
|
||||
selectedPaletteIndex={c.selectedPaletteIndex}
|
||||
setSelectedPaletteIndex={c.setSelectedPaletteIndex}
|
||||
paletteTrapRef={c.paletteTrapRef}
|
||||
filteredNavItems={c.filteredNavItems}
|
||||
matchedAgents={c.matchedAgents}
|
||||
matchedTasks={c.matchedTasks}
|
||||
matchedRuns={c.matchedRuns}
|
||||
matchedGoals={c.matchedGoals}
|
||||
matchedProjects={c.matchedProjects}
|
||||
hasEntityResults={c.hasEntityResults}
|
||||
paletteActions={c.paletteActions}
|
||||
paletteActionIndex={c.paletteActionIndex}
|
||||
runs={c.runs}
|
||||
navigateTo={c.navigateTo}
|
||||
panelTransition={c.panelTransition}
|
||||
t={c.t}
|
||||
commandPaletteOpen={ui.commandPaletteOpen}
|
||||
setCommandPaletteOpen={ui.setCommandPaletteOpen}
|
||||
commandPaletteSearch={ui.commandPaletteSearch}
|
||||
setCommandPaletteSearch={ui.setCommandPaletteSearch}
|
||||
selectedPaletteIndex={ui.selectedPaletteIndex}
|
||||
setSelectedPaletteIndex={ui.setSelectedPaletteIndex}
|
||||
paletteTrapRef={ui.paletteTrapRef}
|
||||
filteredNavItems={filteredNavItems}
|
||||
matchedAgents={matchedAgents}
|
||||
matchedTasks={matchedTasks}
|
||||
matchedRuns={matchedRuns}
|
||||
matchedGoals={matchedGoals}
|
||||
matchedProjects={matchedProjects}
|
||||
hasEntityResults={hasEntityResults}
|
||||
paletteActions={paletteActions}
|
||||
paletteActionIndex={paletteActionIndex}
|
||||
runs={company.runs}
|
||||
navigateTo={navigateTo}
|
||||
panelTransition={ui.panelTransition}
|
||||
t={ui.t}
|
||||
/>
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</div>
|
||||
</AppActionsContext.Provider>
|
||||
</TranslationContext.Provider>
|
||||
</LocaleContext.Provider>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user