diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 18f87389ce..dc02d4c461 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -793,11 +793,6 @@ function AppInner() { }; }, [currentProject?.id]); - const workflowStepNameLookup = useMemo( - () => new Map(workflowSteps.map((step) => [step.id, step.name] as const)), - [workflowSteps], - ); - // History-aware modal open handlers — push nav entries for back-navigation. const openDetailTask = useCallback((task: Task | TaskDetail, tab?: Parameters[1], opts?: { origin?: DetailTaskOrigin }) => { modalManager.openDetailTask(task, tab, opts); @@ -1169,7 +1164,6 @@ function AppInner() { subscribePluginEvents, openDetailTask, openFileInBrowser, - workflowStepNameLookup, prAuthAvailable, autoMerge, settingsLoaded, @@ -1326,7 +1320,7 @@ function AppInner() { markGitHubStarPromptShown, setShowGitHubStarPrompt, }; - const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openFileInBrowser, openSettings: (section?: string) => openSettingsWithNav(section as SectionId), onOpenUsage: openUsageWithNav, onOpenActivityLog: openActivityLogWithNav, onOpenGitHubImport: openGitHubImportWithNav, onOpenGitManager: openGitManagerWithNav, onOpenSchedules: openSchedulesWithNav, onSendSelectionToTask: modalManager.openNewTaskWithDescription, onCreateTaskFromInsight: handleInsightTaskCreate, onNavigateToMission: handleOpenMission, onTaskCreated: (task: Task) => ingestCreatedTasks([task]), workflowStepNameLookup, prAuthAvailable, autoMerge, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, todosEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible }); + const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openFileInBrowser, openSettings: (section?: string) => openSettingsWithNav(section as SectionId), onOpenUsage: openUsageWithNav, onOpenActivityLog: openActivityLogWithNav, onOpenGitHubImport: openGitHubImportWithNav, onOpenGitManager: openGitManagerWithNav, onOpenSchedules: openSchedulesWithNav, onSendSelectionToTask: modalManager.openNewTaskWithDescription, onCreateTaskFromInsight: handleInsightTaskCreate, onNavigateToMission: handleOpenMission, onTaskCreated: (task: Task) => ingestCreatedTasks([task]), prAuthAvailable, autoMerge, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, todosEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible }); return ( diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index a4e6c5155c..cae1299938 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -7,7 +7,7 @@ import "./Board.css"; import type { ToastType } from "../hooks/useToast"; import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; -import { fetchWorkflowSteps, promoteTask, type ModelInfo, type BoardWorkflowsPayload } from "../api"; +import { promoteTask, type ModelInfo, type BoardWorkflowsPayload } from "../api"; import { useBlockerFanout } from "../hooks/useBlockerFanout"; import { MOBILE_MEDIA_QUERY, useViewportMode } from "../hooks/useViewportMode"; import { recordResumeEvent } from "../utils/resumeInstrumentation"; @@ -89,7 +89,6 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean { return previous.every((task, index) => task === next[index]); } -const EMPTY_WORKFLOW_STEP_NAME_LOOKUP: ReadonlyMap = new Map(); let boardWasPreviouslyInactive = false; // Real mobile browsers can pan the document horizontally while focusing/clicking @@ -123,14 +122,6 @@ function scheduleDocumentHorizontalScrollReset() { setTimeout(run, 0); } -function areWorkflowNameLookupsEqual(previous: ReadonlyMap, next: ReadonlyMap): boolean { - if (previous.size !== next.size) return false; - for (const [key, value] of previous) { - if (next.get(key) !== value) return false; - } - return true; -} - function BoardWorkflowSkeleton({ empty = false }: { empty?: boolean }) { return (
@@ -148,7 +139,6 @@ function BoardWorkflowSkeleton({ empty = false }: { empty?: boolean }) { export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowColumnsEnabled, settingsLoaded, workflowControlsInHeader = false }: BoardProps) { const [archivedCollapsed, setArchivedCollapsed] = useState(true); const archivedLoadedRef = useRef(false); - const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP); const boardRef = useRef(null); const [headerWorkflowSlot, setHeaderWorkflowSlot] = useState(() => { if (typeof document === "undefined") return null; @@ -242,28 +232,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask return stableGrouped; }, [tasks]); - useEffect(() => { - let cancelled = false; - - fetchWorkflowSteps(projectId) - .then((steps) => { - if (cancelled) return; - - const nextLookup = new Map(steps.map((step) => [step.id, step.name] as const)); - setWorkflowStepNameLookup((previous) => ( - areWorkflowNameLookupsEqual(previous, nextLookup) ? previous : nextLookup - )); - }) - .catch(() => { - if (cancelled) return; - setWorkflowStepNameLookup((previous) => (previous.size === 0 ? previous : EMPTY_WORKFLOW_STEP_NAME_LOOKUP)); - }); - - return () => { - cancelled = true; - }; - }, [projectId]); - // FN-4574 + FN-001 diagnosis: on iOS Safari, the mobile board can occasionally // snap against stale layout/visualViewport metrics before flex columns resolve, // both on initial mount and on pageshow/bfcache restore after backgrounding. @@ -602,7 +570,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} - workflowStepNameLookup={workflowStepNameLookup} taskCardFieldDefs={taskCardFieldDefs} blockerFanoutMap={blockerFanoutMap} prAuthAvailable={prAuthAvailable} @@ -649,7 +616,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} - workflowStepNameLookup={workflowStepNameLookup} taskCardFieldDefs={taskCardFieldDefs} blockerFanoutMap={blockerFanoutMap} prAuthAvailable={prAuthAvailable} @@ -695,7 +661,6 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} - workflowStepNameLookup={workflowStepNameLookup} taskCardFieldDefs={taskCardFieldDefs} blockerFanoutMap={blockerFanoutMap} prAuthAvailable={prAuthAvailable} diff --git a/packages/dashboard/app/components/Column.tsx b/packages/dashboard/app/components/Column.tsx index 4eaa9057c1..4066bf1edc 100644 --- a/packages/dashboard/app/components/Column.tsx +++ b/packages/dashboard/app/components/Column.tsx @@ -139,8 +139,6 @@ interface ColumnProps { onOpenMission?: (missionId: string) => void; /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ lastFetchTimeMs?: number; - /** Lookup of workflow step IDs to display names, fetched once at board level. */ - workflowStepNameLookup?: ReadonlyMap; /** Per-task card-placed custom field definitions (U13/KTD-14). */ taskCardFieldDefs?: ReadonlyMap; /** Precomputed blocker fanout keyed by blocker task ID. */ @@ -173,7 +171,7 @@ interface ColumnProps { getDraggingTaskId?: () => string | null; } -function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, workflowStepNameLookup, taskCardFieldDefs, blockerFanoutMap, prAuthAvailable, workflowMode, workflowId, columnDisplayName, columnFlags, onPromote, canDropTask, getDraggingTaskId }: ColumnProps) { +function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, blockerFanoutMap, prAuthAvailable, workflowMode, workflowId, columnDisplayName, columnFlags, onPromote, canDropTask, getDraggingTaskId }: ColumnProps) { const { t } = useTranslation("app"); // Anchor the board.rejection.* catalog keys for the i18next extractor (it // scopes `t` to the useTranslation binding, so the shared translateRejection @@ -728,7 +726,6 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} - workflowStepNameLookup={workflowStepNameLookup} taskCardFieldDefs={taskCardFieldDefs} blockerFanoutMap={blockerFanoutMap} prAuthAvailable={prAuthAvailable} @@ -762,7 +759,6 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onPromote={isHoldColumn && onPromote ? handlePromote : undefined} isPromoting={isHoldColumn && onPromote ? promotingIds.has(task.id) : undefined} lastFetchTimeMs={lastFetchTimeMs} - workflowStepNameLookup={workflowStepNameLookup} cardFieldDefs={taskCardFieldDefs?.get(task.id)} fanout={blockerFanoutMap?.get(task.id)} prAuthAvailable={prAuthAvailable} diff --git a/packages/dashboard/app/components/Lane.tsx b/packages/dashboard/app/components/Lane.tsx index 0479f755a6..e9cf8ee9a9 100644 --- a/packages/dashboard/app/components/Lane.tsx +++ b/packages/dashboard/app/components/Lane.tsx @@ -66,7 +66,6 @@ export interface LaneProps { taskStuckTimeoutMs?: number; onOpenMission?: (missionId: string) => void; lastFetchTimeMs?: number; - workflowStepNameLookup?: ReadonlyMap; /** Per-task card-placed custom field definitions (U13/KTD-14). */ taskCardFieldDefs?: ReadonlyMap; blockerFanoutMap?: ReadonlyMap; @@ -202,7 +201,6 @@ function LaneComponent(props: LaneProps) { taskStuckTimeoutMs={props.taskStuckTimeoutMs} onOpenMission={props.onOpenMission} lastFetchTimeMs={props.lastFetchTimeMs} - workflowStepNameLookup={props.workflowStepNameLookup} taskCardFieldDefs={props.taskCardFieldDefs} blockerFanoutMap={props.blockerFanoutMap} prAuthAvailable={props.prAuthAvailable} diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 98b2514e8e..b17018d3af 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -1045,14 +1045,24 @@ The execution-time badge is part of the footer's bottom-right chip cluster, so i background: var(--text-dim); } +/* +FNXC:WorkflowSteps 2026-06-25-00:00: +`failed` is a blocking gate failure (red/error); `advisory_failure` is a non-blocking REVISE that +returned feedback but does not block merge (amber/warning). `running` is a graph node actively +executing. These map 1:1 to the unified progress status so the dot color encodes blocking-ness. +*/ .card-step-dot--failed { background: var(--color-error-dark); } -.card-step-dot--workflow-failed { +.card-step-dot--advisory_failure { background: var(--ws-warning); } +.card-step-dot--running { + background: var(--todo); +} + .card-step-name { flex: 1; min-width: 0; diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 6400b3b02e..2bba79dc1f 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -401,8 +401,6 @@ interface TaskCardProps { isPromoting?: boolean; /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ lastFetchTimeMs?: number; - /** Lookup of workflow step IDs to display names, fetched once at board level. */ - workflowStepNameLookup?: ReadonlyMap; /** Disable card drag semantics when embedding in custom draggable containers (e.g. dependency graph). */ disableDrag?: boolean; /** Downstream fan-out entry for this task, computed at board-level. */ @@ -597,7 +595,6 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo previous.onMoveTask === next.onMoveTask && previous.onPromote === next.onPromote && previous.isPromoting === next.isPromoting && - previous.workflowStepNameLookup === next.workflowStepNameLookup && previous.disableDrag === next.disableDrag && previous.fanout?.totalCount === next.fanout?.totalCount && previous.fanout?.activeTodoCount === next.fanout?.activeTodoCount && @@ -707,7 +704,6 @@ function TaskCardComponent({ onPromote, isPromoting = false, lastFetchTimeMs, - workflowStepNameLookup, disableDrag, fanout, prAuthAvailable, @@ -1072,8 +1068,8 @@ function TaskCardComponent({ return providers; }, [task.modelProvider, task.validatorModelProvider, task.planningModelProvider]); const unifiedProgress = useMemo( - () => getUnifiedTaskProgress(task, workflowStepNameLookup), - [task.steps, task.enabledWorkflowSteps, task.workflowStepResults, workflowStepNameLookup], + () => getUnifiedTaskProgress(task), + [task.steps, task.enabledWorkflowSteps, task.workflowStepResults], ); const showProgressSection = unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress"); @@ -2283,12 +2279,18 @@ function TaskCardComponent({ {showSteps && (
{unifiedProgress.items.map((step) => { - const isWorkflowFailed = step.source === "workflow" && step.status === "failed"; - + /* + FNXC:WorkflowSteps 2026-06-25-00:00: + The dot color is keyed by the unified status, which now distinguishes the two + workflow-failure modes: `advisory_failure` (non-blocking REVISE → amber/warning) vs + `failed` (blocking gate failure → red/error). `running` shows the in-progress color. + No `card-step-dot--workflow-failed` override is needed — the status class carries the + distinction directly. + */ return (