diff --git a/.changeset/fn-173-removal.md b/.changeset/fn-173-removal.md new file mode 100644 index 0000000000..63f5f3b943 --- /dev/null +++ b/.changeset/fn-173-removal.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Replace duplicate Keep buttons with dismissible duplicate tags. +category: feature +dev: Removes five Keep i18n keys while retaining the dismissNearDuplicate seam. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index b19ebc27b3..6ef455108a 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -722,7 +722,7 @@ Default notes: | `autoArchiveDoneTasksEnabled` | `boolean` | `true` | Enable periodic auto-archiving of done tasks; tasks with live lineage children are retained for operator action. | | `autoArchiveDoneAfterMs` | `number` | `172800000` | Age in ms after entering done before auto-archive (48h). | | `doneAutoArchiveDays` | `number` | `0` | Integer day-based done-task retention. `0` disables day override; values `> 0` take precedence over `autoArchiveDoneAfterMs`. | -| `autoArchiveDuplicateTasksEnabled` | `boolean` | `false` | FN-7658/FN-8401: gates whether same-agent duplicate intake on every create backend auto-archives the later/new task. Default `false` — the duplicate is flagged in place (`nearDuplicateOf`/`nearDuplicateScore` marker, yellow "Duplicate" chip with Keep/Archive actions), and no live sibling is deleted or archived automatically. Set `true` to restore opt-in archival of the new task only. Does not affect ghost-bug preflight or tombstone-resurrection blocking. | +| `autoArchiveDuplicateTasksEnabled` | `boolean` | `false` | FN-7658/FN-8401: gates whether same-agent duplicate intake on every create backend auto-archives the later/new task. Default `false` — the duplicate is flagged in place (`nearDuplicateOf`/`nearDuplicateScore` marker, yellow "Duplicate" chip with a clear-the-flag control and Archive action), and no live sibling is deleted or archived automatically. Set `true` to restore opt-in archival of the new task only. Does not affect ghost-bug preflight or tombstone-resurrection blocking. | | `archiveAgentLogMode` | `"none" \| "compact" \| "full"` | `"compact"` | Agent log retention strategy for cold archive snapshots. | | `autoUpdatePrStatus` | `boolean` | `false` | Auto-refresh PR status badges. | | `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. Suppressed when the source issue is also the task's *tracked* issue (`githubTracking.enabled` with the same `owner/repo#number`): the GitHub tracking comment already reports completion there, with commit/branch/PR/files details, so the issue would otherwise receive two comments. In that case `githubCommentTemplate` is not used and the task log records `Skipped GitHub issue completion comment`. When tracking points at a *different* issue, both issues are commented as before. | @@ -1921,7 +1921,7 @@ One terminal `tool_error` after the current execution-run cursor therefore quali Choose the alternate model with the standard provider-aware selector in **Settings → Models · Project**; clearing it removes both persisted pair keys, and incomplete legacy pairs display as unset. **Settings → Scheduling** retains the enable toggle, optional node target, and retry policy. Escalation is enabled only when the toggle is true and either a complete provider/model pair or a node ID is configured. It is single-shot: after FN-7996 exhausts same-model retries, Fusion persists the override and tries once before the existing terminal park. The alternate model enters the [model-selection hierarchy](#model-selection-hierarchy) as a task-level override; a node target enters `resolveEffectiveNode` as a task-level routing override and is requeued so scheduler routing is recalculated. This remains opt-in by default to avoid unexpected model cost or execution behavior. Column-agent overrides still govern their sessions and can supersede a task-level model target. -| `triageDuplicateResolution` | `"prompt" \| "keep" \| "delete"` | `"prompt"` | Controls `DUPLICATE: FN-NNNN` markers emitted during triage. **prompt** flags and system-pauses the task for an operator Keep/Delete decision; the existing decision banner links to the canonical task. **keep** dismisses the marker and replans a real task. **delete** restores legacy auto-delete behavior. | +| `triageDuplicateResolution` | `"prompt" \| "keep" \| "delete"` | `"prompt"` | Controls `DUPLICATE: FN-NNNN` markers emitted during triage. **prompt** flags and system-pauses the task until an operator clears the flag or deletes the task; the existing decision banner links to the canonical task. **keep** dismisses the marker and replans a real task. **delete** restores legacy auto-delete behavior. | ### `mobileNavPrimaryItems` diff --git a/docs/task-management.md b/docs/task-management.md index ab8938aa73..63491b243a 100644 --- a/docs/task-management.md +++ b/docs/task-management.md @@ -94,7 +94,7 @@ Near-duplicate flagging now keeps the task in its normal flow column (`todo` / a - `source.sourceMetadata.nearDuplicateOf = ` - `source.sourceMetadata.nearDuplicateScore = ` - `source.sourceMetadata.nearDuplicateSharedTokens = ` -- optional `source.sourceMetadata.nearDuplicateDismissed = true` after user chooses Keep +- optional `source.sourceMetadata.nearDuplicateDismissed = true` after the operator clears the duplicate flag - activity event `task:near-duplicate-flagged` A near-duplicate flag is only actionable while the canonical task is active. The triage backstop does not persist `nearDuplicateOf` for archived, soft-deleted, done, or missing canonicals; when a canonical later becomes inactive through archive, soft-delete, or move-to-done, the store clears `nearDuplicateOf`, `nearDuplicateScore`, `nearDuplicateSharedTokens`, and `nearDuplicateDismissed` from active referrers and records an informational log entry without pausing or failing those tasks. @@ -102,7 +102,7 @@ A near-duplicate flag is only actionable while the canonical task is active. The Dashboard surfaces this as a yellow Duplicate chip plus modal actions only while the canonical exists and is active: - **Archive** (user-initiated archive path) -- **Keep** (dismisses the warning by setting `nearDuplicateDismissed: true`) +- **Clear the duplicate flag** (dismisses the warning by setting `nearDuplicateDismissed: true`) This layer complements, rather than replaces, FN-4829 similarity detection, FN-4918 deterministic deduplication, and FN-4892 same-agent intake heuristics. @@ -144,7 +144,7 @@ Layer behavior: - **Triage planning loop** — after triage reads the generated `PROMPT.md`, an exact redirect marker short-circuits directly into `finalizeApprovedTask()`. Normal plans run deterministic spec hygiene checks in triage, then the selected workflow's optional Plan Review gate owns AI plan review before execution. - **Self-healing sweep** — maintenance Batch 2 runs `resolveExplicitDuplicateMarkerTasks()` across `triage`/`todo` tasks to clean up older stuck marker tasks. The sweep is best-effort, capped at 50 marker tasks per cycle, and can be disabled with the internal setting `resolveExplicitDuplicateMarkerEnabled: false` (default `true`). -An operator's decision is durable for a task and its active canonical pair. **Keep** records the acknowledgement, clears the marker-only prompt and triage decision hold, and lets planning continue; triage and self-healing will not ask again if that same marker is reprocessed. A marker for a different active canonical remains a new decision. **Delete** for an explicit-marker decision soft-deletes the duplicate, while **Archive** for an ordinary near-duplicate leaves it terminal in Archived; neither outcome is reopened as a duplicate decision. +An operator's decision is durable for a task and its active canonical pair. **Clearing the duplicate flag** records the acknowledgement, retires the marker source, clears the triage decision hold, and lets planning continue; triage and self-healing will not ask again if that same marker is reprocessed. A marker for a different active canonical remains a new decision. **Delete** for an explicit-marker decision soft-deletes the duplicate, while **Archive** for an ordinary near-duplicate leaves it terminal in Archived; neither outcome is reopened as a duplicate decision. All three layers fail open: parse errors, task lookup failures, file-read failures, activity-recording errors, or other unexpected exceptions log a warning and continue normal intake/triage/self-healing flow instead of blocking task creation or recovery. @@ -162,7 +162,7 @@ Fusion applies two conservative intake heuristics that may auto-archive newly fi - **Ghost-bug preflight** (triage finalize path): for bug-fix-shaped specs that cite concrete constructs/commands, Fusion probes current `main`. If all definitive probes show the cited bug does not reproduce, the task is archived as `auto-resolved-ghost-bug`. - **Same-agent duplicate intake** (all task-create backends): if the same `source.sourceAgentId` (or `source.sourceParentTaskId`) filed a highly similar task within 24h (threshold `0.75`), Fusion still detects the near-duplicate — but what happens next depends on the `autoArchiveDuplicateTasksEnabled` project/global setting (default **`false`**, FN-7658/FN-8401): - - **Default (`false`)**: the later task is left in place and flagged via the same near-duplicate marker used elsewhere (`sourceMetadata.nearDuplicateOf` / `nearDuplicateScore`), so the dashboard's yellow "Duplicate" chip with Keep/Archive actions surfaces it for a human decision. Neither the new task nor its live siblings are moved to `archived` or deleted automatically. + - **Default (`false`)**: the later task is left in place and flagged via the same near-duplicate marker used elsewhere (`sourceMetadata.nearDuplicateOf` / `nearDuplicateScore`), so the dashboard's yellow "Duplicate" chip with a clear-the-flag control and Archive action surfaces it for a human decision. Neither the new task nor its live siblings are moved to `archived` or deleted automatically. - **`true`** (legacy behavior, opt-in): only the later/new task is archived as `auto-resolved-duplicate`; its live siblings remain intact. Ghost-bug preflight is unaffected by `autoArchiveDuplicateTasksEnabled` — it is a distinct heuristic and always auto-archives on a definitive non-repro. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 8260273216..df03df8f69 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1592,7 +1592,7 @@ function AppInner() { // Props for the extracted switch (see components/dashboard/MainContent.tsx). // Every value is passed by its App name; the switch renders the same subtrees as before. - const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, columnFlagsByTaskId: footerColumnFlagsByTaskId, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openTaskPopup: popOutTaskDetailForCurrentView, onOpenSessionInNewWindow: openSessionInNewWindow, openMobileTasksInPopup, openFileInBrowser, onMoveTask: moveTask, onDeleteTask: deleteTask, onArchiveTask: archiveTask, onRevertTask: revertTask, onMergeTask: mergeTask, onRetryTask: retryTask, onPauseTask: pauseTask, onUnpauseTask: unpauseTask, onBypassReview: bypassReview, onResetTask: resetTask, onDuplicateTask: duplicateTask, onTaskUpdated: (task: Task) => ingestCreatedTasks([task]), 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, taskDetailChatFirst, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible }); + const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, columnFlagsByTaskId: footerColumnFlagsByTaskId, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openTaskPopup: popOutTaskDetailForCurrentView, onOpenSessionInNewWindow: openSessionInNewWindow, openMobileTasksInPopup, openFileInBrowser, onMoveTask: moveTask, onUpdateTask: updateTask, onDeleteTask: deleteTask, onArchiveTask: archiveTask, onRevertTask: revertTask, onMergeTask: mergeTask, onRetryTask: retryTask, onPauseTask: pauseTask, onUnpauseTask: unpauseTask, onBypassReview: bypassReview, onResetTask: resetTask, onDuplicateTask: duplicateTask, onTaskUpdated: (task: Task) => ingestCreatedTasks([task]), 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, taskDetailChatFirst, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible }); /* FNXC:OpenTasksInRightSidebar 2026-06-28-00:00: diff --git a/packages/dashboard/app/components/DockTaskList.tsx b/packages/dashboard/app/components/DockTaskList.tsx index 7376da64c2..6de0b12a4c 100644 --- a/packages/dashboard/app/components/DockTaskList.tsx +++ b/packages/dashboard/app/components/DockTaskList.tsx @@ -14,6 +14,7 @@ export interface DockTaskListProps { projectId?: string; onOpenTask?: (task: Task | TaskDetail) => void; onReviseTask?: (task: Task) => void; + onUpdateTask?: (id: string, updates: { title?: string; description?: string; dependencies?: string[]; dismissNearDuplicate?: boolean; githubTracking?: { enabled?: boolean } }) => Promise; onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; removeLineageReferences?: boolean; githubIssueAction?: GithubIssueAction; allowResurrection?: boolean }) => Promise; addToast?: (message: string, type?: ToastType) => void; prAuthAvailable?: boolean; @@ -45,10 +46,15 @@ export function DockTaskList({ columnFlagsByTaskId, onOpenTask, onDeleteTask, onReviseTask, + onUpdateTask, addToast = () => {}, prAuthAvailable = false, autoMergeEnabled = false, }: DockTaskListProps) { + /* + FNXC:NearDuplicateDetection 2026-08-23-04:10: + A host that renders a duplicate tag must also pass its clear action, otherwise a triage hold has no UI release. + */ const { t } = useTranslation("app"); const [showDone, setShowDone] = useState(false); @@ -113,7 +119,7 @@ export function DockTaskList({ columnFlagsByTaskId, {revertedTasks.length > 0 && (

{t("tasks.revertedTasks", "Reverted Tasks")}

- {revertedTasks.map((task) => )} + {revertedTasks.map((task) => )}
)} {isEmpty ? ( @@ -138,6 +144,7 @@ export function DockTaskList({ columnFlagsByTaskId, Every task Delete affordance must reach the shared confirm→delete flow. The right-dock Tasks list is a TaskCard host, so it must pass onDeleteTask instead of rendering cards that silently lack/delete-disable the destructive path. */ onDeleteTask={onDeleteTask} + onUpdateTask={onUpdateTask} addToast={addToast} prAuthAvailable={prAuthAvailable} autoMergeEnabled={autoMergeEnabled} diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index b7474eb7c3..69c948b3c1 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -1178,8 +1178,14 @@ The execution-time badge is part of the footer's bottom-right chip cluster, so i text-decoration: none; } -.card-duplicate-chip, -.card-duplicate-keep { +.card-duplicate-chip-group { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + flex-shrink: 0; +} + +.card-duplicate-chip { display: inline-flex; align-items: center; gap: var(--space-xs); @@ -1216,30 +1222,41 @@ The execution-time badge is part of the footer's bottom-right chip cluster, so i color: var(--color-warning); } -.card-duplicate-keep { - border-color: color-mix(in srgb, var(--color-warning) 45%, transparent); - background: color-mix(in srgb, var(--surface-2) 90%, transparent); - color: var(--text); +.card-duplicate-dismiss { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--card-chip-height); + min-height: var(--card-chip-height); + padding: var(--space-xs); + border: var(--btn-border-width) solid color-mix(in srgb, var(--color-warning) 45%, transparent); + border-radius: var(--radius-pill); + background: color-mix(in srgb, var(--color-warning) 16%, transparent); + color: var(--color-warning); cursor: pointer; - appearance: none; - background-image: none; - transition: - background-color var(--transition-fast), - color var(--transition-fast), - box-shadow var(--transition-fast); + transition: background-color var(--transition-fast), box-shadow var(--transition-fast); } -.card-duplicate-keep:hover { +.card-duplicate-dismiss::after { + content: ""; + position: absolute; + inset: calc(var(--space-sm) * -1); +} + +.card-duplicate-dismiss:hover { background: var(--card-hover); } -.card-duplicate-keep:focus-visible { +.card-duplicate-dismiss:focus-visible { box-shadow: var(--focus-ring-strong); outline: none; } @media (max-width: 768px) { - .card-duplicate-chip { + .card-duplicate-chip, + .card-duplicate-chip-group, + .card-duplicate-dismiss { max-width: 100%; } diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 5c453efa07..4dc470891e 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { memo, useCallback, useState, useRef, useEffect, useLayoutEffect, useMemo, type CSSProperties, type ReactElement } from "react"; import { createPortal } from "react-dom"; -import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight, Eye, MoreHorizontal, Sparkles } from "lucide-react"; +import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight, Eye, MoreHorizontal, Sparkles, X } from "lucide-react"; import type { Task, TaskDetail, Column, ColumnId, PrInfo, IssueInfo, TaskPriority, GithubIssueAction, MergeResult, PlannerOversightLevel } from "@fusion/core"; import { DEFAULT_PLANNER_OVERSIGHT_LEVEL, @@ -2264,9 +2264,9 @@ function TaskCardComponent({ try { await onUpdateTask(task.id, { dismissNearDuplicate: true }); - addToast(t("tasks.duplicateDismissed", "Kept {{taskId}}; duplicate warning dismissed", { taskId: task.id }), "success"); + addToast(t("tasks.duplicateDismissed", "Duplicate flag cleared for {{taskId}}", { taskId: task.id }), "success"); } catch (err) { - addToast(t("tasks.keepFailed", "Failed to keep {{taskId}}: {{error}}", { taskId: task.id, error: getErrorMessage(err) }), "error"); + addToast(t("tasks.duplicateDismissFailed", "Failed to clear the duplicate flag for {{taskId}}: {{error}}", { taskId: task.id, error: getErrorMessage(err) }), "error"); } }, [addToast, onUpdateTask, task.id]); @@ -3272,26 +3272,35 @@ function TaskCardComponent({ )} {showNearDuplicateChip && ( - <> + /* + FNXC:NearDuplicateDetection 2026-08-23-04:10: + FN-173 removed the undiscoverable Keep decision button. The tag is an acknowledgeable notification; + clearing it deliberately uses dismissNearDuplicate so triage-marker holds release instead of stranding paused cards. + */ + {t("tasks.duplicateOf", "Duplicate of {{id}}", { id: String(task.sourceMetadata?.nearDuplicateOf) })} {onUpdateTask && ( )} - + )} {chipFarRight && (showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue && (