import "./TaskDetailModal.css"; import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles } from "lucide-react"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import { useColumnLabel } from "../i18n/labels"; import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Task, TaskDetail, TaskAttachment, Column, ColumnId, MergeResult, Settings, GlobalSettings, AgentLogEntry, Agent, TaskPriority, TaskSourceIssue, WorkflowStepResult, GithubIssueAction } from "@fusion/core"; import { DEFAULT_TASK_PRIORITY, REPO_OVERRIDE_RE, TASK_PRIORITIES, VALID_TRANSITIONS, isColumn, getErrorMessage, resolveTaskExecutionModel, resolveTaskPlanningModel, resolveTaskValidatorModel, } from "@fusion/core"; import { resolveEffectiveAutoMerge } from "../../../core/src/task-merge"; import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask, fetchTaskDetail, fetchSettings, fetchGlobalSettings, requestSpecRevision, rebuildTaskSpec, approvePlan, rejectPlan, refineTask, fetchWorkflowResults, assignTask, fetchAgents, fetchAgent, recoverBranchBinding, refreshPrStatus, fetchBoardWorkflows, updateTaskCustomFields, summarizeTitle, api } from "../api"; import type { RecoverBranchBindingOutcome, WorkflowFieldDefinition, CustomFieldRejection } from "../api"; import { ApiRequestError } from "../api"; import { TaskFieldsSection } from "./TaskFieldsSection"; import type { ToastType } from "../hooks/useToast"; import { useAgentLogs } from "../hooks/useAgentLogs"; import { useConfirm } from "../hooks/useConfirm"; import { AgentLogViewer } from "./AgentLogViewer"; import { ModelSelectorTab } from "./ModelSelectorTab"; import { PrPanel } from "./PrPanel"; import { PrCreateModal } from "./PrCreateModal"; import { TaskComments } from "./TaskComments"; import { TaskReviewTab } from "./TaskReviewTab"; import { MergeDetails } from "./MergeDetails"; import { TaskChangesTab } from "./TaskChangesTab"; import { TaskForm, type PendingImage } from "./TaskForm"; import { useNodes } from "../hooks/useNodes"; import { WorkflowResultsTab } from "./WorkflowResultsTab"; import { RoutingTab } from "./RoutingTab"; import { TaskDocumentsTab } from "./TaskDocumentsTab"; import { TaskTokenStatsPanel } from "./TaskTokenStatsPanel"; import { BranchGroupCard } from "./BranchGroupCard"; import { PluginSlot } from "./PluginSlot"; import { ProviderIcon } from "./ProviderIcon"; import { subscribeSse } from "../sse-bus"; import type { SessionTerminalMode, SessionTerminalPosture } from "./SessionTerminal"; import { usePluginUiSlots } from "../hooks/usePluginUiSlots"; import { appendTokenQuery } from "../auth"; import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete"; import { MAX_AUTO_MERGE_RETRIES, computeBlockerFanoutMap } from "../hooks/useBlockerFanout"; import { resolveEffectiveGithubRepoDefault } from "./githubTracking"; import type { TFunction } from "i18next"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../utils/inReviewStallCopy"; import { getStalePausedReviewCopy, shouldShowStalePausedReviewBadge } from "../utils/stalePausedReviewCopy"; import { getTaskAgeStalenessCopy } from "../utils/taskAgeStalenessCopy"; import { findInReviewStallLogEntry, IN_REVIEW_STALL_LOG_REGEX } from "../utils/findInReviewStallLogEntry"; interface ModelSelection { provider?: string; modelId?: string; } const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]); const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/; const markdownLinkifyComponents: Components = { p: ({ children, ...props }) =>
{linkifyReactChildren(children)}
, li: ({ children, ...props }) =>{children};
}
return {linkedChildren};
},
};
/**
* Resolve the effective executor model following the engine's resolution order:
* 1. Per-task modelProvider/modelId (both must be set)
* 2. Project/global execution lane fallback
*/
function extractExecutorModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) {
if (entry.agent !== "executor" || entry.type !== "text") continue;
const match = entry.text.match(/^Executor using model: (.+?)\/(.+)$/);
if (match) {
result = { provider: match[1], modelId: match[2] };
}
}
return result;
}
function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provider: string; modelId: string } | null {
let result: { provider: string; modelId: string } | null = null;
for (const entry of entries) {
if (entry.agent !== "reviewer" || entry.type !== "text") continue;
const match = entry.text.match(/^Reviewer using model: (.+?)\/(.+)$/);
if (match) {
result = { provider: match[1], modelId: match[2] };
}
}
return result;
}
function hasUsableTrackingTitle(task: { title?: string | null; description?: string | null }): boolean {
if ((task.title ?? "").trim().length > 0) {
return true;
}
const firstMeaningfulLine = (task.description ?? "")
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0);
return Boolean(firstMeaningfulLine);
}
function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSelection {
const runtimeConfig = (agent?.runtimeConfig ?? undefined) as Record{t("taskDetail.nearDuplicate.copy", "This task appears to be a near-duplicate of")}{" "} {". "}{t("taskDetail.nearDuplicate.actions", "Choose Archive to move this task to archived, or Keep to continue with this task.")}
{task.worktrunkFailure.stderr.slice(0, 2048)}
)}
{t("taskDetail.retries.capReached", "Retry cap reached for this task.")}
)}{t("taskDetail.branchBinding.copy", "This in-review task isn't currently attached to a fusion branch. If a live fusion branch still exists for it, you can reattach it here.")}
{recoverBranchBindingOutcome && ({t("taskDetail.refine.help", "Describe what needs to be refined or improved...")}