Files
fusion/packages/dashboard/app/hooks/useModalManager.ts
Fusion Agent eb3eeb887a FN-074: remove task splitting and parent deletion
Remove complex-task splitting and parent-deletion behavior across core, dashboard, and engine.

- Remove subtask splitting, breakdown UI, and related API routes and lifecycle handling.
- Add the migration and prompt/documentation updates that enforce the simplified task model.
- Update affected tests and integrations for the removed behavior.

Files changed:
 .changeset/fn-074-removal.md                       |    7 +
 docs/dashboard-guide.md                            |   17 +-
 docs/gitlab-parity-inventory.md                    |    7 +-
 docs/mcp.md                                        |    6 +-
 docs/settings-reference.md                         |   16 +-
 docs/task-management.md                            |   31 +-
 docs/workflow-steps.md                             |   10 +-
 .../cli/skill/fusion/references/task-structure.md  |    2 +-
 .../cli/skill/fusion/workflows/specifications.md   |    4 +-
 packages/core/src/__tests__/agent-prompts.test.ts  |   19 +-
 .../builtin-workflow-settings-triage.test.ts       |   32 +-
 .../src/__tests__/postgres/schema-applier.test.ts  |    2 +-
 .../core/src/__tests__/prompt-overrides.test.ts    |    3 +-
 ...-deleted-observed-dispatch-side-effects.test.ts |    3 -
 .../task-deleted-outbox-consumer-backoff.test.ts   |    1 -
 packages/core/src/agents/agent-prompts.ts          |   56 +-
 packages/core/src/index.ts                         |    2 +-
 .../0062_remove_task_subtask_splitting.sql         |    2 +
 packages/core/src/postgres/schema-applier.ts       |   14 +-
 packages/core/src/postgres/schema/project.ts       |    1 -
 packages/core/src/store.ts                         |   12 +-
 packages/core/src/task-delete-attribution.ts       |   20 +-
 packages/core/src/task-delete-notice.ts            |    7 +-
 .../__tests__/lifecycle-outbox-writer.test.ts      |    4 +-
 .../core/src/task-store/archive-lifecycle-2.ts     |   13 +-
 packages/core/src/task-store/archive-lifecycle.ts  |    6 +-
 packages/core/src/task-store/lifecycle-outbox.ts   |    2 -
 packages/core/src/task-store/persistence.ts        |    2 -
 packages/core/src/task-store/serialization.ts      |    2 -
 packages/core/src/task-store/task-creation.ts      |    2 -
 .../src/task-store/task-deleted-outbox-consumer.ts |    4 -
 packages/core/src/task-store/task-mutation-ops.ts  |    2 +-
 packages/core/src/task-store/task-row-mappers.ts   |    2 +-
 packages/core/src/tasks/prompt-overrides.ts        |   35 -
 packages/core/src/types.ts                         |   12 -
 packages/core/src/types/mesh/archive-planning.ts   |    1 -
 packages/core/src/types/task/task-core.ts          |    3 -
 .../src/workflows/builtin-workflow-settings.ts     |   86 --
 packages/dashboard/app/App.tsx                     |   15 -
 .../mission-planning-modals-mobile.test.ts         |   15 -
 packages/dashboard/app/api/legacy.ts               |   18 +-
 packages/dashboard/app/api/planning/ai-text.ts     |  171 +---
 packages/dashboard/app/api/planning/planning.ts    |   46 -
 packages/dashboard/app/api/tasks/tasks.ts          |    4 +-
 packages/dashboard/app/components/AppModals.tsx    |   39 +-
 packages/dashboard/app/components/Board.tsx        |   10 +-
 packages/dashboard/app/components/Column.tsx       |    9 +-
 .../dashboard/app/components/InlineCreateCard.tsx  |   63 +-
 packages/dashboard/app/components/Lane.tsx         |    3 +-
 packages/dashboard/app/components/ListView.tsx     |   10 +-
 packages/dashboard/app/components/NewTaskModal.tsx |    8 +-
 .../dashboard/app/components/PlanningModeModal.css |  128 ---
 .../dashboard/app/components/PlanningModeModal.tsx |  566 +----------
 .../dashboard/app/components/QuickEntryBox.tsx     |   43 +-
 .../dashboard/app/components/SettingsModal.tsx     |    2 -
 .../app/components/SubtaskBreakdownModal.tsx       |  932 -----------------
 packages/dashboard/app/components/TaskCard.tsx     |    1 -
 packages/dashboard/app/components/TaskForm.tsx     |   23 +-
 .../__tests__/AgentPromptsManager.test.tsx         |    1 -
 .../app/components/__tests__/AppModals.test.tsx    |   92 +-
 .../app/components/__tests__/Board.test.tsx        |   69 +-
 .../components/__tests__/InlineCreateCard.test.tsx |  170 ----
 .../app/components/__tests__/ListView.test.tsx     |   38 -
 .../app/components/__tests__/NewTaskModal.test.tsx |   48 -
 .../components/__tests__/QuickEntryBox.test.tsx    |  293 +-----
 .../__tests__/SubtaskBreakdownModal.test.tsx       |  933 -----------------
 ...skDetail.mobile-transition.board-panel.test.tsx |    1 -
 .../__tests__/TaskDetail.swipe-back.test.tsx       |    1 -
 .../app/components/__tests__/TaskForm.test.tsx     |   67 --
 .../__tests__/WorkflowSettingsPanel.test.tsx       |   51 -
 .../app/components/__tests__/board-mobile.test.tsx |   20 -
 .../composer-settings-read-isolation.test.tsx      |   30 -
 .../components/__tests__/migratedModalFixtures.tsx |    2 -
 .../app/components/dashboard/MainContent.tsx       |    5 -
 .../dashboard/__tests__/DashboardBanners.test.tsx  |    1 -
 .../__tests__/MainContent.graph-popout.test.tsx    |    1 -
 .../dashboard/app/components/dashboard/types.ts    |    2 -
 .../app/components/workflow-setting-display.ts     |   10 -
 ...ckgroundSessions.resume-instrumentation.test.ts |    1 -
 .../hooks/__tests__/useBackgroundSessions.test.ts  |   25 +-
 .../app/hooks/__tests__/useModalManager.test.ts    |    3 -
 packages/dashboard/app/hooks/modalPersistence.ts   |   14 -
 .../dashboard/app/hooks/useBackgroundSessions.ts   |    9 +-
 packages/dashboard/app/hooks/useModalManager.ts    |   58 +-
 packages/dashboard/app/hooks/useProjectActions.ts  |    2 +-
 packages/dashboard/app/hooks/useTaskHandlers.ts    |    9 -
 packages/dashboard/app/utils/builtinPrompts.ts     |    7 -
 packages/dashboard/app/utils/projectStorage.ts     |    1 -
 .../src/__tests__/ai-session-diagnostics.test.ts   |    5 +-
 .../src/__tests__/github-tracking-state.test.ts    |   97 --
 .../src/__tests__/gitlab-delete-close.test.ts      |    6 +-
 .../gitlab-parity-inventory-documentation.test.ts  |    1 -
 .../src/__tests__/gitlab-split-close.test.ts       |  125 ---
 .../planning-flow-diagnostics-guardrail.test.ts    |    1 -
 .../src/__tests__/process-lifecycle.test.ts        |    1 -
 .../dashboard/src/__tests__/routes-auth.test.ts    |    2 -
 .../src/__tests__/routes-automation.test.ts        |    2 -
 .../dashboard/src/__tests__/routes-git.test.ts     |    2 -
 .../dashboard/src/__tests__/routes-github.test.ts  |   92 --
 .../src/__tests__/routes-planning-tracking.test.ts |  130 ---
 .../src/__tests__/routes-planning.test.ts          | 1062 +-------------------
 .../dashboard/src/__tests__/routes-system.test.ts  |    2 -
 .../dashboard/src/__tests__/routes-tasks.test.ts   |  902 -----------------
 .../shared-branch-group-entry-points.test.ts       |   93 --
 packages/dashboard/src/ai-session-diagnostics.ts   |    3 +-
 packages/dashboard/src/ai-session-store.ts         |    6 +-
 packages/dashboard/src/ai-session-timeout.ts       |    3 +-
 packages/dashboard/src/github-tracking-state.ts    |   43 +-
 packages/dashboard/src/gitlab-delete-close.ts      |    7 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |    4 +-
 packages/dashboard/src/gitlab-split-close.ts       |  103 --
 packages/dashboard/src/gitlab-tracking-state.ts    |    2 +-
 packages/dashboard/src/index.ts                    |    1 -
 packages/dashboard/src/planning.ts                 |  171 ----
 packages/dashboard/src/routes.ts                   |    6 -
 ...er-planning-subtask-routes.parent-close.test.ts |  120 ---
 .../dashboard/src/routes/register-git-github.ts    |    6 -
 .../src/routes/register-planning-subtask-routes.ts |  773 +-------------
 .../src/routes/register-task-workflow-routes.ts    |    6 +-
 packages/dashboard/src/server.ts                   |    8 -
 packages/dashboard/src/subtask-breakdown.ts        |  833 ---------------
 packages/dashboard/src/triage-trait.ts             |   93 +-
 .../src/__tests__/agent-task-read-tools.test.ts    |    1 -
 .../triage-duplicate-search-regression.test.ts     |    3 -
 .../triage-split-into-subtasks-delete.test.ts      |  222 ----
 packages/engine/src/__tests__/triage.test.ts       |  519 +---------
 packages/engine/src/triage.ts                      |  239 +----
 127 files changed, 214 insertions(+), 10001 deletions(-)

Fusion-Task-Id: FN-074

Fusion-Task-Lineage: 23207799-4ec4-4ec7-bcd7-83cba4d98f53

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-21 04:32:53 +00:00

646 lines
26 KiB
TypeScript

import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import type { Task, TaskDetail } from "@fusion/core";
import type { SectionId } from "../components/SettingsModal";
import type { ToastType } from "./useToast";
import { removeScopedItem } from "../utils/projectStorage";
import { readBoardWorkflowSelection } from "../utils/boardWorkflowSelection";
import { applyLocalTaskPatch } from "./useTasks";
/*
FNXC:TaskDetailActivity 2026-06-30-22:15:
Keep `chat` as the public initial-tab id for the renamed Activity task-detail tab so existing dashboard callers and deep links remain compatible until the future planner Chat tab ships under its own contract. Legacy `logs` callers are also preserved by TaskDetailModal, which opens Activity → Feed instead of rendering a top-level Logs tab.
*/
export type DetailTaskTab =
| "summary"
/*
FNXC:TaskRecommendations 2026-08-08-07:15:
Recommendations are a shared completed-task detail tab. Keep the public modal-manager union in
sync so every dashboard host can request it without falling back to a local-only tab type.
*/
| "recommendations"
| "chat"
| "definition"
| "logs"
/*
FNXC:SharedBranchPromotionAdvisories 2026-08-08-02:16:
FN-8823 routes landed-member promotion advisories directly to their persisted
Review items, so every Task Detail host must accept the existing review tab.
*/
| "changes"
| "review"
| "comments"
| "model"
| "workflow"
| "pr"
| "retries";
export type DetailTaskOrigin = "list-mobile";
export type DetailTaskInitialAction = "refine";
export interface DetailTaskOpenOptions {
origin?: DetailTaskOrigin;
initialAction?: DetailTaskInitialAction;
}
export interface DetailTaskInitialActionRequest {
action: DetailTaskInitialAction;
requestId: number;
}
interface UseModalManagerOptions {
projectId?: string;
planningSessions: Array<{ id: string }>;
}
/**
* State and handler contract for App-level modal/overlay orchestration.
*/
export interface ModalManager {
// State
newTaskModalOpen: boolean;
newTaskInitialDescription: string | null;
newTaskInitialWorkflowId: string | null | undefined;
isPlanningOpen: boolean;
planningInitialPlan: string | null;
planningSourceIssue: { provider: "github"; repository: string; issueNumber: number; url: string; title?: string } | undefined;
planningResumeSessionId: string | undefined;
planningWorkflowId: string | null | undefined;
/*
FNXC:PlanningKeepAlive 2026-07-22-12:20:
Monotonic counter bumped by every payload-carrying planning entry point (initial plan handoff, resume/session open). The kept-alive embedded Planning instance keys on it so explicit handoffs remount with the pre-keep-alive fresh-open semantics (auto-start, session load), while plain sidebar navigation (openPlanning/closePlanning) leaves it untouched and restores the live instance.
*/
planningEntryGeneration: number;
// Can be Task (optimistic open) or TaskDetail (full data with prompt)
detailTask: (Task | TaskDetail) | null;
detailTaskInitialTab: DetailTaskTab | undefined;
detailTaskInitialAction: DetailTaskInitialActionRequest | null;
detailTaskOrigin: DetailTaskOrigin | null;
groupModalGroupId: string | null;
settingsOpen: boolean;
settingsInitialSection: SectionId | undefined;
schedulesOpen: boolean;
githubImportOpen: boolean;
usageOpen: boolean;
usageAnchorRect: DOMRect | null;
terminalOpen: boolean;
terminalInitialCommand: string | undefined;
terminalInitialCommandGeneration: number;
filesOpen: boolean;
fileBrowserWorkspace: string;
fileBrowserInitialFile: string | null;
activityLogOpen: boolean;
gitManagerOpen: boolean;
workflowEditorOpen: boolean;
/** When the workflow editor opens, which internal panel to pre-select (U9 redirect stubs). */
workflowEditorInitialPanel?: "settings";
/** When the workflow editor opens, which modal action to start. */
workflowEditorInitialAction?: "create";
/** When the workflow editor opens for editing, which workflow id to pre-select. */
workflowEditorInitialWorkflowId?: string;
agentsOpen: boolean;
scriptsOpen: boolean;
setupWizardOpen: boolean;
modelOnboardingOpen: boolean;
anyModalOpen: boolean;
// Handlers
openNewTask: (workflowId?: string | null) => void;
openNewTaskWithDescription: (description: string) => void;
closeNewTask: () => void;
openPlanning: () => void;
openPlanningWithInitialPlan: (initialPlan: string, workflowId?: string | null, sourceIssue?: { provider: "github"; repository: string; issueNumber: number; url: string; title?: string }) => void;
resumePlanning: () => void;
openPlanningWithSession: (sessionId: string) => void;
/**
FNXC:PlanningModals 2026-07-23-00:00:
One-shot consumption of the seeded initial plan. Embedded Planning calls this the moment its
auto-start fires; the payload must not survive that start, because Planning unmounts on
main-content navigation and a still-set planningInitialPlan re-auto-started a duplicate
planning session on every navigate-back remount.
*/
clearPlanningInitialPlan: () => void;
closePlanning: () => void;
openDetailTask: (
task: Task | TaskDetail,
initialTab?: DetailTaskTab,
options?: DetailTaskOpenOptions,
) => void;
openDetailWithChangesTab: (task: Task | TaskDetail) => void;
updateDetailTask: (updated: Partial<TaskDetail>) => void;
closeDetailTask: () => void;
openGroupModal: (groupId: string) => void;
closeGroupModal: () => void;
openSettings: (section?: SectionId) => void;
/*
FNXC:Settings 2026-06-22-00:00:
Sets the Settings initial/active section WITHOUT opening the modal overlay. Used by the embedded main-content Settings view so header/sidebar/deep-link entry points can carry a requested section while navigating to taskView === "settings" instead of mounting the dialog.
*/
setSettingsSection: (section?: SectionId) => void;
closeSettings: () => void;
openSchedules: () => void;
closeSchedules: () => void;
openGitHubImport: () => void;
closeGitHubImport: () => void;
openUsage: (anchorRect?: DOMRect | null) => void;
closeUsage: () => void;
toggleTerminal: () => void;
closeTerminal: () => void;
openFiles: (workspace?: string, initialFile?: string | null) => void;
closeFiles: () => void;
setFileWorkspace: (workspace: string) => void;
openActivityLog: () => void;
closeActivityLog: () => void;
openGitManager: () => void;
closeGitManager: () => void;
openWorkflowEditor: (initialPanelOrAction?: "settings" | "create", initialWorkflowId?: string) => void;
closeWorkflowEditor: () => void;
openAgents: () => void;
closeAgents: () => void;
openScripts: () => void;
closeScripts: () => void;
runScript: (name: string, command: string) => Promise<void>;
openSetupWizard: () => void;
closeSetupWizard: () => void;
openModelOnboarding: () => void;
closeModelOnboarding: () => void;
/*
FNXC:ProjectSwitchModalReset 2026-07-23-00:00:
Switching the active project must dismiss modals that show the previous project's data
(task detail, group, new task, GitHub import, files, git manager,
activity log, workflow editor, scripts, terminal) and drop pending planning payloads so
Planning does not reopen the old project's plan. Cross-project modals (settings,
schedules, usage, agents, setup wizard, model onboarding) stay open.
*/
closeProjectScopedModals: () => void;
onPlanningTaskCreated: (task: Task, addToast: (message: string, type?: ToastType) => void) => void;
onPlanningTasksCreated: (tasks: Task[], addToast: (message: string, type?: ToastType) => void) => void;
}
/**
* Centralized modal manager for dashboard App-level UI state.
*
* Encapsulates all modal open/close booleans, related resume/initial payloads,
* and cross-modal transitions (for example, script runner -> terminal handoff).
*/
export function useModalManager(options: UseModalManagerOptions): ModalManager {
const { t } = useTranslation("app");
const { planningSessions } = options;
const [newTaskModalOpen, setNewTaskModalOpen] = useState(false);
const [newTaskInitialDescription, setNewTaskInitialDescription] = useState<string | null>(null);
const [newTaskInitialWorkflowId, setNewTaskInitialWorkflowId] = useState<string | null | undefined>(undefined);
const [isPlanningOpen, setIsPlanningOpen] = useState(false);
const [planningInitialPlan, setPlanningInitialPlan] = useState<string | null>(null);
const [planningResumeSessionId, setPlanningResumeSessionId] = useState<string | undefined>(undefined);
const [planningWorkflowId, setPlanningWorkflowId] = useState<string | null | undefined>(undefined);
const [planningSourceIssue, setPlanningSourceIssue] = useState<{ provider: "github"; repository: string; issueNumber: number; url: string; title?: string } | undefined>(undefined);
// FNXC:PlanningKeepAlive 2026-07-22-12:20: see ModalManager.planningEntryGeneration.
const [planningEntryGeneration, setPlanningEntryGeneration] = useState(0);
// Can be Task (optimistic open) or TaskDetail (full data with prompt)
const [detailTask, setDetailTask] = useState<(Task | TaskDetail) | null>(null);
/**
* FNXC:TaskDetailTabs 2026-06-17-00:00:
* FN-6532 makes Chat the default task-detail view whenever a task opens without an explicit tab request.
*
* FNXC:TaskDetailSummaryTab 2026-06-27-00:00:
* Store omitted task-detail tabs as `undefined` so done tasks can resolve the implicit landing tab to Summary without stealing explicit Chat requests.
*/
const [detailTaskInitialTab, setDetailTaskInitialTab] = useState<DetailTaskTab | undefined>(undefined);
/*
FNXC:DoneTaskRefine 2026-07-01-00:00:
Done-task card/list context menus must open the existing Task Detail refinement modal after right-click or long-press. Store refinement as a one-shot action request with a monotonically increasing id so selecting Refine again for an already-open task reopens the composer without duplicating API/form logic outside TaskDetailContent.
*/
const [detailTaskInitialAction, setDetailTaskInitialAction] = useState<DetailTaskInitialActionRequest | null>(null);
const detailTaskInitialActionRequestIdRef = useRef(0);
const [detailTaskOrigin, setDetailTaskOrigin] = useState<DetailTaskOrigin | null>(null);
const [groupModalGroupId, setGroupModalGroupId] = useState<string | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
const [schedulesOpen, setSchedulesOpen] = useState(false);
const [githubImportOpen, setGitHubImportOpen] = useState(false);
const [usageOpen, setUsageOpen] = useState(false);
const [usageAnchorRect, setUsageAnchorRect] = useState<DOMRect | null>(null);
const [terminalOpen, setTerminalOpen] = useState(false);
const [terminalInitialCommand, setTerminalInitialCommand] = useState<string | undefined>(undefined);
const [terminalInitialCommandGeneration, setTerminalInitialCommandGeneration] = useState(0);
const [filesOpen, setFilesOpen] = useState(false);
const [fileBrowserWorkspace, setFileBrowserWorkspace] = useState("project");
const [fileBrowserInitialFile, setFileBrowserInitialFile] = useState<string | null>(null);
const [activityLogOpen, setActivityLogOpen] = useState(false);
const [gitManagerOpen, setGitManagerOpen] = useState(false);
const [workflowEditorOpen, setWorkflowEditorOpen] = useState(false);
const [workflowEditorInitialPanel, setWorkflowEditorInitialPanel] = useState<"settings" | undefined>(undefined);
const [workflowEditorInitialAction, setWorkflowEditorInitialAction] = useState<"create" | undefined>(undefined);
const [workflowEditorInitialWorkflowId, setWorkflowEditorInitialWorkflowId] = useState<string | undefined>(undefined);
const [agentsOpen, setAgentsOpen] = useState(false);
const [scriptsOpen, setScriptsOpen] = useState(false);
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
const [modelOnboardingOpen, setModelOnboardingOpen] = useState(false);
const anyModalOpen = Boolean(
detailTask ||
groupModalGroupId ||
settingsOpen ||
newTaskModalOpen ||
/*
FNXC:Navigation 2026-06-21-00:00:
FN-6886 reuses Planning Mode state only as docked-view payload storage, so it must not make the app behave as though a blocking modal overlay is open.
*/
terminalOpen ||
filesOpen ||
activityLogOpen ||
gitManagerOpen ||
workflowEditorOpen ||
scriptsOpen ||
agentsOpen ||
usageOpen ||
schedulesOpen ||
githubImportOpen ||
setupWizardOpen ||
modelOnboardingOpen,
);
/*
FNXC:TaskWorkflowSelection 2026-08-18-03:33:
The New Task dialog must inherit the currently selected board workflow on every open surface.
Explicit workflow ids and explicit null take precedence, while the aggregate sentinel never crosses
this task-creation boundary.
*/
const resolveNewTaskInitialWorkflowId = useCallback((workflowId: unknown): string | null | undefined => {
if (typeof workflowId === "string" || workflowId === null) return workflowId;
return readBoardWorkflowSelection(options.projectId) ?? undefined;
}, [options.projectId]);
const openNewTask = useCallback((workflowId?: string | null) => {
setNewTaskInitialDescription(null);
setNewTaskInitialWorkflowId(resolveNewTaskInitialWorkflowId(workflowId));
setNewTaskModalOpen(true);
}, [resolveNewTaskInitialWorkflowId]);
const openNewTaskWithDescription = useCallback((description: string) => {
setNewTaskInitialDescription(description);
setNewTaskInitialWorkflowId(resolveNewTaskInitialWorkflowId(undefined));
setNewTaskModalOpen(true);
}, [resolveNewTaskInitialWorkflowId]);
const closeNewTask = useCallback(() => {
setNewTaskModalOpen(false);
setNewTaskInitialDescription(null);
setNewTaskInitialWorkflowId(undefined);
}, []);
const openPlanning = useCallback(() => {
// FNXC:PlanningModals 2026-06-20-20:10:
// A fresh planning open must clear any resume-session id / initial plan left
// by a prior resumePlanning/openPlanningWith* flow; otherwise the modal reopens
// into the stale session or pre-fills an old plan instead of starting blank.
setPlanningResumeSessionId(undefined);
setPlanningInitialPlan(null);
setPlanningWorkflowId(undefined);
setPlanningSourceIssue(undefined);
setIsPlanningOpen(true);
}, []);
const openPlanningWithInitialPlan = useCallback((initialPlan: string, workflowId?: string | null, sourceIssue?: { provider: "github"; repository: string; issueNumber: number; url: string; title?: string }) => {
// FNXC:PlanningModals 2026-06-20-20:10: clear a stale resume-session id so the
// supplied initial plan is honored rather than being overridden by an old session.
setPlanningResumeSessionId(undefined);
setPlanningInitialPlan(initialPlan);
setPlanningWorkflowId(workflowId);
setPlanningSourceIssue(sourceIssue);
// FNXC:PlanningKeepAlive 2026-07-22-12:20: payload-carrying entries bump the generation so the kept-alive instance remounts with fresh-open semantics.
setPlanningEntryGeneration((generation) => generation + 1);
setIsPlanningOpen(true);
}, []);
const resumePlanning = useCallback(() => {
const session = planningSessions[0];
if (!session) return;
setPlanningWorkflowId(undefined);
setPlanningSourceIssue(undefined);
setPlanningResumeSessionId(session.id);
setPlanningEntryGeneration((generation) => generation + 1);
setIsPlanningOpen(true);
}, [planningSessions]);
const openPlanningWithSession = useCallback((sessionId: string) => {
setPlanningWorkflowId(undefined);
setPlanningSourceIssue(undefined);
setPlanningResumeSessionId(sessionId);
setPlanningEntryGeneration((generation) => generation + 1);
setIsPlanningOpen(true);
}, []);
const clearPlanningInitialPlan = useCallback(() => {
// FNXC:GitHubPlanningSourceIssue 2026-08-09-08:09: The seed and its GitHub
// provenance are one atomic handoff. After auto-start consumes the seed, retaining
// sourceIssue would incorrectly attach it to a later plan started in the same view.
setPlanningInitialPlan(null);
setPlanningSourceIssue(undefined);
}, []);
const closePlanning = useCallback(() => {
setIsPlanningOpen(false);
setPlanningInitialPlan(null);
setPlanningResumeSessionId(undefined);
setPlanningWorkflowId(undefined);
setPlanningSourceIssue(undefined);
}, []);
const openDetailTask = useCallback((
task: Task | TaskDetail,
initialTab?: DetailTaskTab,
options?: DetailTaskOpenOptions,
) => {
setDetailTask(task);
setDetailTaskInitialTab(initialTab);
setDetailTaskInitialAction(options?.initialAction ? { action: options.initialAction, requestId: detailTaskInitialActionRequestIdRef.current += 1 } : null);
setDetailTaskOrigin(options?.origin ?? null);
}, []);
const openDetailWithChangesTab = useCallback((task: Task | TaskDetail) => {
setDetailTask(task);
setDetailTaskInitialTab("changes");
setDetailTaskInitialAction(null);
setDetailTaskOrigin(null);
}, []);
/*
FNXC:TaskDetailStateStability 2026-08-09-07:13:
This callback receives locally-authored patches from the open detail view, not competing server
snapshots. FN-5148 pins the id rule: reject an explicit foreign id but accept an absent id; FN-8796
must not turn absent/equal local clocks into stale evidence. AppModals owns live board/SSE arbitration.
*/
const updateDetailTask = useCallback((updated: Partial<TaskDetail>) => {
setDetailTask((prev) => {
if (!prev || (updated.id !== undefined && updated.id !== prev.id)) return prev;
return applyLocalTaskPatch(prev, { ...updated, id: prev.id });
});
}, []);
const closeDetailTask = useCallback(() => {
setDetailTask(null);
setDetailTaskInitialAction(null);
setDetailTaskOrigin(null);
}, []);
const openGroupModal = useCallback((groupId: string) => {
setGroupModalGroupId(groupId);
}, []);
const closeGroupModal = useCallback(() => {
setGroupModalGroupId(null);
}, []);
const openSettings = useCallback((section?: SectionId) => {
setSettingsInitialSection(section);
setSettingsOpen(true);
}, []);
const setSettingsSection = useCallback((section?: SectionId) => {
setSettingsInitialSection(section);
}, []);
const closeSettings = useCallback(() => {
setSettingsOpen(false);
setSettingsInitialSection(undefined);
}, []);
const openSchedules = useCallback(() => setSchedulesOpen(true), []);
const closeSchedules = useCallback(() => setSchedulesOpen(false), []);
const openGitHubImport = useCallback(() => setGitHubImportOpen(true), []);
const closeGitHubImport = useCallback(() => setGitHubImportOpen(false), []);
const openUsage = useCallback((anchorRect?: DOMRect | null) => {
setUsageAnchorRect(anchorRect ?? null);
setUsageOpen(true);
}, []);
const closeUsage = useCallback(() => {
setUsageOpen(false);
setUsageAnchorRect(null);
}, []);
const toggleTerminal = useCallback(() => {
setTerminalOpen((prev) => !prev);
}, []);
const closeTerminal = useCallback(() => {
setTerminalOpen(false);
setTerminalInitialCommand(undefined);
}, []);
const openFiles = useCallback((workspace?: string, initialFile?: string | null) => {
if (typeof workspace === "string" && workspace) {
setFileBrowserWorkspace(workspace);
}
if (typeof initialFile === "string" || initialFile === null) {
setFileBrowserInitialFile(initialFile);
} else {
setFileBrowserInitialFile(null);
}
setFilesOpen(true);
}, []);
const closeFiles = useCallback(() => {
setFilesOpen(false);
setFileBrowserInitialFile(null);
}, []);
const setFileWorkspace = useCallback((workspace: string) => {
if (typeof workspace === "string" && workspace) {
setFileBrowserWorkspace(workspace);
}
}, []);
const openActivityLog = useCallback(() => setActivityLogOpen(true), []);
const closeActivityLog = useCallback(() => setActivityLogOpen(false), []);
const openGitManager = useCallback(() => setGitManagerOpen(true), []);
const closeGitManager = useCallback(() => setGitManagerOpen(false), []);
const openWorkflowEditor = useCallback((initialPanelOrAction?: "settings" | "create", initialWorkflowId?: string) => {
const isSettingsOpen = initialPanelOrAction === "settings";
const isCreateOpen = initialPanelOrAction === "create";
setWorkflowEditorInitialPanel(isSettingsOpen ? "settings" : undefined);
setWorkflowEditorInitialAction(isCreateOpen ? "create" : undefined);
setWorkflowEditorInitialWorkflowId(!isSettingsOpen && !isCreateOpen ? initialWorkflowId : undefined);
setWorkflowEditorOpen(true);
}, []);
const closeWorkflowEditor = useCallback(() => {
setWorkflowEditorOpen(false);
setWorkflowEditorInitialPanel(undefined);
setWorkflowEditorInitialAction(undefined);
setWorkflowEditorInitialWorkflowId(undefined);
}, []);
const openAgents = useCallback(() => setAgentsOpen(true), []);
const closeAgents = useCallback(() => setAgentsOpen(false), []);
const openScripts = useCallback(() => setScriptsOpen(true), []);
const closeScripts = useCallback(() => setScriptsOpen(false), []);
const runScript = useCallback(async (_name: string, command: string) => {
setScriptsOpen(false);
setTerminalInitialCommand(command);
setTerminalInitialCommandGeneration((generation) => generation + 1);
setTerminalOpen(true);
}, []);
const openSetupWizard = useCallback(() => setSetupWizardOpen(true), []);
const closeSetupWizard = useCallback(() => setSetupWizardOpen(false), []);
const openModelOnboarding = useCallback(() => setModelOnboardingOpen(true), []);
const closeModelOnboarding = useCallback(() => setModelOnboardingOpen(false), []);
/*
FNXC:ProjectSwitchModalReset 2026-07-23-00:00:
Project swap left the previous project's modals open (a task-detail modal for project A
kept rendering over project B's board) and kept planning resume/initial-plan payloads,
so the docked Planning view re-entered project A's session. Close every project-scoped
modal and clear their payloads in one transition; deliberately leave settings,
schedules, usage, agents, setup wizard, and model onboarding alone — they are not
project-scoped surfaces.
*/
const closeProjectScopedModals = useCallback(() => {
setDetailTask(null);
setDetailTaskInitialTab(undefined);
setDetailTaskInitialAction(null);
setDetailTaskOrigin(null);
setGroupModalGroupId(null);
setNewTaskModalOpen(false);
setNewTaskInitialDescription(null);
setNewTaskInitialWorkflowId(undefined);
setIsPlanningOpen(false);
setPlanningInitialPlan(null);
setPlanningSourceIssue(undefined);
setPlanningResumeSessionId(undefined);
setPlanningWorkflowId(undefined);
setGitHubImportOpen(false);
setFilesOpen(false);
setFileBrowserInitialFile(null);
setActivityLogOpen(false);
setGitManagerOpen(false);
setWorkflowEditorOpen(false);
setWorkflowEditorInitialPanel(undefined);
setWorkflowEditorInitialAction(undefined);
setWorkflowEditorInitialWorkflowId(undefined);
setScriptsOpen(false);
setTerminalOpen(false);
setTerminalInitialCommand(undefined);
}, []);
const clearQuickAddPlanningDrafts = useCallback(() => {
/*
FNXC:QuickAddPlanningPreserve 2026-06-22-00:00:
Planning completion, not planning exit, is the only modal-manager transition that clears preserved quick-add drafts. Use the active project id so scoped drafts are removed from the correct workspace.
*/
removeScopedItem("kb-quick-entry-text", options.projectId);
removeScopedItem("kb-inline-create-text", options.projectId);
}, [options.projectId]);
const onPlanningTaskCreated = useCallback((task: Task, addToast: (message: string, type?: ToastType) => void) => {
addToast(t("modalManager.createdFromPlanning", "Created {{id}} from planning mode", { id: task.id }), "success");
clearQuickAddPlanningDrafts();
setIsPlanningOpen(false);
setPlanningInitialPlan(null);
}, [clearQuickAddPlanningDrafts, t]);
const onPlanningTasksCreated = useCallback((tasks: Task[], addToast: (message: string, type?: ToastType) => void) => {
const ids = tasks.map((task) => task.id).join(", ");
addToast(t("modalManager.createdMultipleFromPlanning", "Created {{ids}} from planning mode", { ids }), "success");
clearQuickAddPlanningDrafts();
setIsPlanningOpen(false);
setPlanningInitialPlan(null);
}, [clearQuickAddPlanningDrafts, t]);
return {
newTaskModalOpen,
newTaskInitialDescription,
newTaskInitialWorkflowId,
isPlanningOpen,
planningInitialPlan,
planningSourceIssue,
planningResumeSessionId,
planningWorkflowId,
planningEntryGeneration,
detailTask,
detailTaskInitialTab,
detailTaskInitialAction,
detailTaskOrigin,
groupModalGroupId,
settingsOpen,
settingsInitialSection,
schedulesOpen,
githubImportOpen,
usageOpen,
usageAnchorRect,
terminalOpen,
terminalInitialCommand,
terminalInitialCommandGeneration,
filesOpen,
fileBrowserWorkspace,
fileBrowserInitialFile,
activityLogOpen,
gitManagerOpen,
workflowEditorOpen,
workflowEditorInitialPanel,
workflowEditorInitialAction,
workflowEditorInitialWorkflowId,
agentsOpen,
scriptsOpen,
setupWizardOpen,
modelOnboardingOpen,
anyModalOpen,
openNewTask,
openNewTaskWithDescription,
closeNewTask,
openPlanning,
openPlanningWithInitialPlan,
resumePlanning,
openPlanningWithSession,
clearPlanningInitialPlan,
closePlanning,
openDetailTask,
openDetailWithChangesTab,
updateDetailTask,
closeDetailTask,
openGroupModal,
closeGroupModal,
openSettings,
setSettingsSection,
closeSettings,
openSchedules,
closeSchedules,
openGitHubImport,
closeGitHubImport,
openUsage,
closeUsage,
toggleTerminal,
closeTerminal,
openFiles,
closeFiles,
setFileWorkspace,
openActivityLog,
closeActivityLog,
openGitManager,
closeGitManager,
openWorkflowEditor,
closeWorkflowEditor,
openAgents,
closeAgents,
openScripts,
closeScripts,
runScript,
openSetupWizard,
closeSetupWizard,
openModelOnboarding,
closeModelOnboarding,
closeProjectScopedModals,
onPlanningTaskCreated,
onPlanningTasksCreated,
};
}