diff --git a/.changeset/remove-stuck-task-tagging.md b/.changeset/remove-stuck-task-tagging.md new file mode 100644 index 0000000000..f7af0a38c2 --- /dev/null +++ b/.changeset/remove-stuck-task-tagging.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Remove stuck-task tagging from the dashboard — no more Stuck badges, card styling, or footer stuck count. +category: feature +dev: "Deletes utils/taskStuck.ts, the stuck ExecutorStats field, and taskStuckTimeoutMs prop plumbing; the setting remains and engine recovery sweeps still consume it. Also repoints the FN-6756 liveness ratchet at the extracted executor session facades." diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 7eeaed9def..f53d97c8ab 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -850,7 +850,6 @@ function AppInner() { showWorktreeGrouping, globalPaused, isTestMode, - taskStuckTimeoutMs, staleHighFanoutBlockerAgeThresholdMs, capacityRiskBannerEnabled, capacityRiskTodoThreshold, @@ -1765,7 +1764,6 @@ function AppInner() { handleOpenDetailWithTab, handleToggleFavorite, handleToggleModelFavorite, - taskStuckTimeoutMs, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, openCreateWorkflowWithNav, @@ -2032,7 +2030,6 @@ function AppInner() { tasks={footerTasks} projectId={currentProject.id} columnFlagsByTaskId={footerColumnFlagsByTaskId} - taskStuckTimeoutMs={taskStuckTimeoutMs} staleHighFanoutBlockerAgeThresholdMs={staleHighFanoutBlockerAgeThresholdMs} lastFetchTimeMs={lastFetchTimeMs} currentProjectPath={currentProject.path} diff --git a/packages/dashboard/app/__tests__/api-projects.test.ts b/packages/dashboard/app/__tests__/api-projects.test.ts index 32b9152265..0568cb361e 100644 --- a/packages/dashboard/app/__tests__/api-projects.test.ts +++ b/packages/dashboard/app/__tests__/api-projects.test.ts @@ -1127,7 +1127,6 @@ describe("ExecutorStats type", () => { const stats: ExecutorStats = { runningTaskCount: 3, blockedTaskCount: 2, - stuckTaskCount: 1, queuedTaskCount: 10, inReviewCount: 4, executorState: "running", @@ -1137,7 +1136,6 @@ describe("ExecutorStats type", () => { expect(stats.runningTaskCount).toBe(3); expect(stats.blockedTaskCount).toBe(2); - expect(stats.stuckTaskCount).toBe(1); expect(stats.queuedTaskCount).toBe(10); expect(stats.inReviewCount).toBe(4); expect(stats.executorState).toBe("running"); @@ -1149,7 +1147,6 @@ describe("ExecutorStats type", () => { const idleStats: ExecutorStats = { runningTaskCount: 0, blockedTaskCount: 0, - stuckTaskCount: 0, queuedTaskCount: 5, inReviewCount: 0, executorState: "idle", @@ -1159,7 +1156,6 @@ describe("ExecutorStats type", () => { const runningStats: ExecutorStats = { runningTaskCount: 2, blockedTaskCount: 1, - stuckTaskCount: 0, queuedTaskCount: 3, inReviewCount: 1, executorState: "running", @@ -1169,7 +1165,6 @@ describe("ExecutorStats type", () => { const pausedStats: ExecutorStats = { runningTaskCount: 1, blockedTaskCount: 0, - stuckTaskCount: 0, queuedTaskCount: 8, inReviewCount: 2, executorState: "paused", @@ -1185,7 +1180,6 @@ describe("ExecutorStats type", () => { const stats: ExecutorStats = { runningTaskCount: 0, blockedTaskCount: 0, - stuckTaskCount: 0, queuedTaskCount: 0, inReviewCount: 0, executorState: "idle", diff --git a/packages/dashboard/app/api/projects/projects.ts b/packages/dashboard/app/api/projects/projects.ts index f52f9963a8..0e73c42f78 100644 --- a/packages/dashboard/app/api/projects/projects.ts +++ b/packages/dashboard/app/api/projects/projects.ts @@ -74,7 +74,7 @@ export type ExecutorState = "idle" | "running" | "paused" | "stopped"; /** Aggregated executor statistics for the status bar. * - * Counts (runningTaskCount, blockedTaskCount, queuedTaskCount, inReviewCount, stuckTaskCount) + * Counts (runningTaskCount, blockedTaskCount, queuedTaskCount, inReviewCount) * are derived client-side from the same tasks array shared with the board, ensuring * the footer counts always match the active work states displayed on screen. Queued covers * todo plus planning/triage work; Done is intentionally not exposed unless a footer Done @@ -97,8 +97,11 @@ export interface ExecutorStats { runningTaskCount: number; /** Number of tasks with blockedBy field set (waiting on file overlap) */ blockedTaskCount: number; - /** Number of "in-progress" tasks with no activity for > 10 minutes */ - stuckTaskCount: number; + /* + FNXC:StuckTagRemoval 2026-08-17-22:30: + Operator removed stuck-task tagging from the dashboard (stuckTaskCount deleted here); + engine recovery sweeps still consume taskStuckTimeoutMs server-side. + */ /** Number of tasks in "todo" plus planning/triage work states */ queuedTaskCount: number; /** Number of tasks in "in-review" column */ diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index b3b49517fd..4558000f87 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -90,8 +90,6 @@ interface BoardProps { favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void; - /** Project-level stuck task timeout in milliseconds (undefined = disabled) */ - taskStuckTimeoutMs?: number; /** Called when user clicks a mission badge on a task card */ onOpenMission?: (missionId: string) => void; /** Age threshold in milliseconds before high fan-out blockers escalate in dashboard surfaces. */ @@ -177,7 +175,7 @@ function columnDefOffersArchiveAllDone(columnDef: { flags: { complete?: boolean; return columnDef.flags.complete === true && columnDef.flags.archived !== true; } -export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onRevertTask, onReviseTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, onLoadMoreArchivedTasks, archivedHasMore, archivedLoadingMore, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowControlsInHeader = false }: BoardProps) { +export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onRevertTask, onReviseTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, onLoadMoreArchivedTasks, archivedHasMore, archivedLoadingMore, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowControlsInHeader = false }: BoardProps) { const [archivedCollapsed, setArchivedCollapsed] = useState(true); /* FNXC:DoneColumnSorting 2026-06-29-16:57: @@ -1001,7 +999,6 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o onToggleFavorite={onToggleFavorite} onToggleModelFavorite={onToggleModelFavorite} isSearchActive={isSearchActive} - taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} taskCardFieldDefs={taskCardFieldDefs} @@ -1104,7 +1101,6 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o onToggleFavorite={onToggleFavorite} onToggleModelFavorite={onToggleModelFavorite} isSearchActive={isSearchActive} - taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} taskCardFieldDefs={taskCardFieldDefs} @@ -1171,7 +1167,6 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o onToggleFavorite={onToggleFavorite} onToggleModelFavorite={onToggleModelFavorite} isSearchActive={isSearchActive} - taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} taskCardFieldDefs={taskCardFieldDefs} diff --git a/packages/dashboard/app/components/Column.tsx b/packages/dashboard/app/components/Column.tsx index 4096b3bc62..19d4f482bf 100644 --- a/packages/dashboard/app/components/Column.tsx +++ b/packages/dashboard/app/components/Column.tsx @@ -182,8 +182,6 @@ interface ColumnProps { onToggleModelFavorite?: (modelId: string) => void; /** When true, search is active — bypass pagination so all matching tasks are visible. */ isSearchActive?: boolean; - /** Project-level stuck task timeout in milliseconds (undefined = disabled) */ - taskStuckTimeoutMs?: number; /** Called when user clicks a mission badge on a task card */ onOpenMission?: (missionId: string) => void; /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ @@ -241,7 +239,7 @@ interface ColumnProps { getDraggingTaskId?: () => string | null; } -function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onRevertTask, onDeleteTask, onArchiveAllDone, doneSortMode, onDoneSortModeChange, collapsed, onToggleCollapse, archivedHasMore, archivedLoadingMore, onLoadMoreArchived, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, taskWorkflowBadges, blockerFanoutMap, prAuthAvailable, holdTaskIds, workflowMode, workflowId, workflowOptions, defaultWorkflowId, columnDisplayName, columnDescription, columnFlags, workflowContextMenuColumns, taskContextMenuColumnsByTaskId, onPromote, canDropTask, getDraggingTaskId }: ColumnProps) { +function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onRevertTask, onDeleteTask, onArchiveAllDone, doneSortMode, onDoneSortModeChange, collapsed, onToggleCollapse, archivedHasMore, archivedLoadingMore, onLoadMoreArchived, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, taskWorkflowBadges, blockerFanoutMap, prAuthAvailable, holdTaskIds, workflowMode, workflowId, workflowOptions, defaultWorkflowId, columnDisplayName, columnDescription, columnFlags, workflowContextMenuColumns, taskContextMenuColumnsByTaskId, 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 @@ -1144,7 +1142,6 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree onRevertTask={onRevertTask} onDeleteTask={onDeleteTask} onOpenDetailWithTab={onOpenDetailWithTab} - taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} taskCardFieldDefs={taskCardFieldDefs} @@ -1187,7 +1184,6 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree onRevertTask={onRevertTask} onDeleteTask={onDeleteTask} onOpenDetailWithTab={onOpenDetailWithTab} - taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} onMoveTask={onMoveTask} taskColumnFlags={getTaskColumnFlags(task)} diff --git a/packages/dashboard/app/components/ExecutorStatusBar.css b/packages/dashboard/app/components/ExecutorStatusBar.css index d0dab310f8..6069df7ad7 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.css +++ b/packages/dashboard/app/components/ExecutorStatusBar.css @@ -99,10 +99,9 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch border-radius: var(--radius-sm); } -.executor-status-bar__segment--stuck { - color: var(--color-error); -} - +/* +FNXC:StuckTagRemoval 2026-08-17-22:30: Operator removed stuck-task tagging from the dashboard; engine recovery sweeps still consume taskStuckTimeoutMs server-side. +*/ .executor-status-bar__segment--fanout { color: var(--color-error); min-width: 0; @@ -136,14 +135,6 @@ FN-6887 makes the footer status bar the canonical desktop/tablet terminal launch background: var(--triage); } -.executor-status-bar__indicator--stuck { - background: var(--color-error); -} - -.executor-status-bar__indicator--stuck.executor-status-bar__indicator--active { - animation: executor-pulse 1s ease-in-out infinite; -} - .executor-status-bar__indicator--queued { background: var(--todo); } diff --git a/packages/dashboard/app/components/ExecutorStatusBar.tsx b/packages/dashboard/app/components/ExecutorStatusBar.tsx index 15ff39861d..b0ed191978 100644 --- a/packages/dashboard/app/components/ExecutorStatusBar.tsx +++ b/packages/dashboard/app/components/ExecutorStatusBar.tsx @@ -18,7 +18,11 @@ import { EngineControlMenu, type EngineControlMenuHandle } from "./EngineControl import { TerminalLauncher } from "./TerminalLauncher"; import { useViewportMode } from "../hooks/useViewportMode"; -type FooterStatId = "queued" | "running" | "stuck" | "blocked" | "fanout"; +/* +FNXC:StuckTagRemoval 2026-08-17-22:30: Operator removed stuck-task tagging from the dashboard; engine recovery sweeps still consume taskStuckTimeoutMs server-side. +The footer no longer renders a Stuck segment or takes taskStuckTimeoutMs. +*/ +type FooterStatId = "queued" | "running" | "blocked" | "fanout"; interface OpenStatTooltip { id: FooterStatId; @@ -63,11 +67,9 @@ interface ExecutorStatusBarProps { projectId?: string; /** Optional task-scoped board trait index supplied by an embedding board. */ columnFlagsByTaskId?: ReadonlyMap; - /** Project-level stuck task timeout in milliseconds (undefined = disabled) */ - taskStuckTimeoutMs?: number; /** Age threshold in milliseconds before high fan-out blockers escalate in dashboard surfaces. */ staleHighFanoutBlockerAgeThresholdMs?: number; - /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ + /** Timestamp (ms) when task data was last confirmed fresh from the server. */ lastFetchTimeMs?: number; /** Absolute path for the currently selected project directory. */ currentProjectPath?: string; @@ -143,7 +145,7 @@ function getStateDisplay(state: ExecutorState, t: TFunction<"app">): { label: st * - Executor state badge (idle/running/paused/stopped) * - Last activity timestamp */ -export function ExecutorStatusBar({ tasks, projectId, columnFlagsByTaskId: suppliedColumnFlagsByTaskId, taskStuckTimeoutMs, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, currentProjectPath, onOpenProjectDirectory, keyboardOpen, hideWhenKeyboardOpen, onToggleTerminal, onOpenScripts, onRunScript, quickChatButtonMode = "off", onOpenQuickChat }: ExecutorStatusBarProps) { +export function ExecutorStatusBar({ tasks, projectId, columnFlagsByTaskId: suppliedColumnFlagsByTaskId, staleHighFanoutBlockerAgeThresholdMs, currentProjectPath, onOpenProjectDirectory, keyboardOpen, hideWhenKeyboardOpen, onToggleTerminal, onOpenScripts, onRunScript, quickChatButtonMode = "off", onOpenQuickChat }: ExecutorStatusBarProps) { const { t } = useTranslation("app"); const viewportMode = useViewportMode(); const isMobile = viewportMode === "mobile"; @@ -159,7 +161,7 @@ export function ExecutorStatusBar({ tasks, projectId, columnFlagsByTaskId: suppl * Settings can route Quick Chat to a footer launcher beside Terminal, keep the draggable floating FAB, or hide the launcher entirely. Footer launch stays desktop/tablet-only like Terminal while mobile opens from the floating path as a full-screen modal. */ const showQuickChatFooterLauncher = !isMobile && quickChatButtonMode === "footer" && Boolean(onOpenQuickChat); - const { stats, loading, error } = useExecutorStats(tasks, projectId, taskStuckTimeoutMs, lastFetchTimeMs, columnFlagsByTaskId); + const { stats, loading, error } = useExecutorStats(tasks, projectId, columnFlagsByTaskId); const [isProjectPathVisible, setIsProjectPathVisible] = useState(false); const [openStatTooltip, setOpenStatTooltip] = useState(null); const engineControlMenuRef = useRef(null); @@ -224,7 +226,7 @@ export function ExecutorStatusBar({ tasks, projectId, columnFlagsByTaskId: suppl staleHighFanoutBlockerAgeThresholdMs ?? STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, /* FNXC:WorkflowResolvedColumns 2026-07-30-23:55: - The same per-task trait index this bar already takes for its stuck/running counts. Without it + The same per-task trait index this bar already takes for its running counts. Without it the fan-out classified against `todo`/`in-review`/`done`, so on a renamed board the overlap bottleneck this segment exists to surface was never detected at all. */ @@ -334,25 +336,6 @@ export function ExecutorStatusBar({ tasks, projectId, columnFlagsByTaskId: suppl {/* Separator */}