diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx index 79be653fb5..667659b7ee 100644 --- a/packages/dashboard/app/components/TaskChatTab.tsx +++ b/packages/dashboard/app/components/TaskChatTab.tsx @@ -177,6 +177,7 @@ function isActiveAgentSession(task: Task | TaskDetail, opts: { sessionLive?: boo const statusAllowsReviewSteering = !task.status || REVIEW_STEERABLE_STATUSES.has(task.status); const columnAllowsSteering = (task.column === "in-progress" && statusAllowsProgressSteering) || (task.column === "in-review" && statusAllowsReviewSteering); + // FNXC:TaskDetailChat 2026-06-20-20:10: // In the default ephemeral-agents mode the scheduler never writes // `assignedAgentId`/`checkedOutBy` — those are only set when // `ephemeralAgentsEnabled === false` (scheduler.ts). An actively-executing diff --git a/packages/dashboard/app/hooks/useModalManager.ts b/packages/dashboard/app/hooks/useModalManager.ts index 57c1ef8f6f..4c9973000e 100644 --- a/packages/dashboard/app/hooks/useModalManager.ts +++ b/packages/dashboard/app/hooks/useModalManager.ts @@ -234,10 +234,19 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager { }, []); 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); setIsPlanningOpen(true); }, []); const openPlanningWithInitialPlan = useCallback((initialPlan: string, workflowId?: string | null) => { + // 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); setIsPlanningOpen(true); @@ -262,6 +271,9 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager { }, []); const openSubtaskBreakdown = useCallback((description: string, workflowId?: string | null) => { + // FNXC:PlanningModals 2026-06-20-20:10: clear a stale subtask resume-session id + // so a new breakdown starts fresh rather than reopening a prior session. + setSubtaskResumeSessionId(undefined); setSubtaskInitialDescription(description); setSubtaskWorkflowId(workflowId); setIsSubtaskOpen(true); diff --git a/packages/dashboard/app/utils/boardWorkflowsCache.ts b/packages/dashboard/app/utils/boardWorkflowsCache.ts index 3fa8ae35f8..78c9f93baa 100644 --- a/packages/dashboard/app/utils/boardWorkflowsCache.ts +++ b/packages/dashboard/app/utils/boardWorkflowsCache.ts @@ -1,4 +1,4 @@ -import type { BoardWorkflowsPayload } from "../api"; +import type { BoardWorkflowDefinition, BoardWorkflowsPayload } from "../api"; const BOARD_WORKFLOWS_CACHE_PREFIX = "fusion:board-workflows:"; const DEFAULT_PROJECT_CACHE_KEY = "default"; @@ -17,6 +17,20 @@ function isBoardWorkflowsPayload(value: unknown): value is BoardWorkflowsPayload if (!Array.isArray(value.workflows)) return false; if (typeof value.defaultWorkflowId !== "string") return false; if (!isRecord(value.taskWorkflowIds)) return false; + // FNXC:BoardWorkflows 2026-06-20-20:10: + // Validate each cached workflow's shape and the taskWorkflowIds value types, + // not just the container types. A malformed entry (e.g. `[{}]` from a stale or + // partially-written cache) would otherwise pass and later throw in Board/ListView + // when accessing workflow.name/columns — re-introducing the legacy flash this + // cache exists to prevent. Mirrors the BoardWorkflowDefinition contract (id, name, + // columns) and the Record taskWorkflowIds map. + if (!value.workflows.every((workflow): workflow is BoardWorkflowDefinition => ( + isRecord(workflow) + && typeof workflow.id === "string" + && typeof workflow.name === "string" + && Array.isArray(workflow.columns) + ))) return false; + if (!Object.values(value.taskWorkflowIds).every((workflowId) => typeof workflowId === "string")) return false; return true; }