import { memo, useEffect, useRef, useState, useMemo } from "react" import { useTranslation } from "react-i18next" import { useCloudUpsell } from "@src/hooks/useCloudUpsell" import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog" import DismissibleUpsell from "@src/components/common/DismissibleUpsell" import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, FoldVertical, ArrowLeft } from "lucide-react" import prettyBytes from "pretty-bytes" import type { ClineMessage } from "@roo-code/types" import { getModelMaxOutputTokens } from "@roo/api" import { findLastIndex } from "@roo/array" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" import { StandardTooltip, Button, Table, TableBody, TableRow, TableCell, CircularProgress } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { vscode } from "@src/utils/vscode" import Thumbnails from "../common/Thumbnails" import { TaskActions } from "./TaskActions" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" import { TodoListDisplay } from "./TodoListDisplay" import { LucideIconButton } from "./LucideIconButton" export interface TaskHeaderProps { task: ClineMessage tokensIn: number tokensOut: number cacheWrites?: number cacheReads?: number totalCost: number aggregatedCost?: number hasSubtasks?: boolean parentTaskId?: string rootOrchestrationCost?: number costBreakdown?: string contextTokens: number buttonsDisabled: boolean handleCondenseContext: (taskId: string) => void todos?: any[] } const TaskHeader = ({ task, tokensIn, tokensOut, cacheWrites, cacheReads, totalCost, aggregatedCost, hasSubtasks, parentTaskId, rootOrchestrationCost, costBreakdown, contextTokens, buttonsDisabled, handleCondenseContext, todos, }: TaskHeaderProps) => { const { t } = useTranslation() const { apiConfiguration, currentTaskItem, clineMessages } = useExtensionState() const { id: modelId, info: model } = useSelectedModel(apiConfiguration) const [isTaskExpanded, setIsTaskExpanded] = useState(false) const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false) const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({ autoOpenOnAuth: false, }) // Check if the task is complete by looking at the last relevant message (skipping resume messages) const isTaskComplete = clineMessages && clineMessages.length > 0 ? (() => { const lastRelevantIndex = findLastIndex( clineMessages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"), ) return lastRelevantIndex !== -1 ? clineMessages[lastRelevantIndex]?.ask === "completion_result" : false })() : false useEffect(() => { const timer = setTimeout(() => { if (currentTaskItem && !isTaskComplete) { setShowLongRunningTaskMessage(true) } }, 120_000) // Show upsell after 2 minutes return () => clearTimeout(timer) }, [currentTaskItem, isTaskComplete]) const textContainerRef = useRef(null) const textRef = useRef(null) const contextWindow = model?.contextWindow || 1 // Calculate maxTokens (reserved for output) once for reuse in percentage and tooltip const maxTokens = useMemo( () => model ? getModelMaxOutputTokens({ modelId, model, settings: apiConfiguration, }) : 0, [model, modelId, apiConfiguration], ) const reservedForOutput = maxTokens || 0 const condenseButton = ( currentTaskItem && handleCondenseContext(currentTaskItem.id)} /> ) const hasTodos = todos && Array.isArray(todos) && todos.length > 0 // Determine if this is a subtask (has a parent) const isSubtask = !!parentTaskId const handleBackToParent = () => { if (parentTaskId) { vscode.postMessage({ type: "showTaskWithId", text: parentTaskId }) } } return (
{isSubtask && (
e.stopPropagation()}>
)} {showLongRunningTaskMessage && !isTaskComplete && ( openUpsell()} dismissOnClick={false} variant="banner"> {t("cloud:upsell.longRunningTask")} )}
{ // Don't expand if clicking on todos section if (e.target instanceof Element && e.target.closest("[data-todo-list]")) { return } // Don't expand if clicking on buttons or interactive elements if ( e.target instanceof Element && (e.target.closest("button") || e.target.closest('[role="button"]') || e.target.closest(".share-button") || e.target.closest("[data-radix-popper-content-wrapper]") || e.target.closest("img") || e.target.tagName === "IMG") ) { return } // Don't expand/collapse if user is selecting text const selection = window.getSelection() if (selection && selection.toString().length > 0) { return } setIsTaskExpanded(!isTaskExpanded) }}>
{isTaskExpanded && {t("chat:task.title")}} {!isTaskExpanded && (
)}
e.stopPropagation()}>
{!isTaskExpanded && contextWindow > 0 && (
e.stopPropagation()}>
{ const availableSpace = contextWindow - (contextTokens || 0) - reservedForOutput return ( {t("chat:tokenProgress.tokensUsedLabel")} {formatLargeNumber(contextTokens || 0)} /{" "} {formatLargeNumber(contextWindow)} {reservedForOutput > 0 && ( {t("chat:tokenProgress.reservedForResponseLabel")} {formatLargeNumber(reservedForOutput)} )} {availableSpace > 0 && ( {t("chat:tokenProgress.availableSpaceLabel")} {formatLargeNumber(availableSpace)} )}
) })()} side="top" sideOffset={8}> {(() => { // Calculate percentage of available input space used // Available input space = context window - reserved for output const availableInputSpace = contextWindow - reservedForOutput const percentage = availableInputSpace > 0 ? Math.round(((contextTokens || 0) / availableInputSpace) * 100) : 0 return ( <> {percentage}% ) })()}
{!!totalCost && ( <> ·
{t("chat:costs.totalWithSubtasks", { cost: (aggregatedCost ?? totalCost).toFixed(2), })}
{costBreakdown && (
{costBreakdown}
)}
) : (
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
) } side="top" sideOffset={8}> <> ${(aggregatedCost ?? totalCost).toFixed(2)} {hasSubtasks && ( * )} )} {typeof rootOrchestrationCost === "number" && rootOrchestrationCost > 0 && ( <> · {t("chat:costs.orchestrationTotal", { cost: rootOrchestrationCost.toFixed(2), })}
} side="top" sideOffset={8}> {t("chat:costs.orchestrationLabel")}: ${rootOrchestrationCost.toFixed(2)} )}
)} {/* Expanded state: Show task text and images */} {isTaskExpanded && ( <>
{task.images && task.images.length > 0 && }
e.stopPropagation()}>
{contextWindow > 0 && ( )} {((typeof cacheReads === "number" && cacheReads > 0) || (typeof cacheWrites === "number" && cacheWrites > 0)) && ( )} {!!totalCost && ( )} {typeof rootOrchestrationCost === "number" && rootOrchestrationCost > 0 && ( )} {/* Size display */} {!!currentTaskItem?.size && currentTaskItem.size > 0 && ( )}
{t("chat:task.contextWindow")}
{condenseButton}
{t("chat:task.tokens")}
{typeof tokensIn === "number" && tokensIn > 0 && ( ↑ {formatLargeNumber(tokensIn)} )} {typeof tokensOut === "number" && tokensOut > 0 && ( ↓ {formatLargeNumber(tokensOut)} )}
{t("chat:task.cache")}
{typeof cacheWrites === "number" && cacheWrites > 0 && ( <> {formatLargeNumber(cacheWrites)} )} {typeof cacheReads === "number" && cacheReads > 0 && ( <> {formatLargeNumber(cacheReads)} )}
{t("chat:task.apiCost")}
{t("chat:costs.totalWithSubtasks", { cost: (aggregatedCost ?? totalCost).toFixed(2), })}
{costBreakdown && (
{costBreakdown}
)} ) : (
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
) } side="top" sideOffset={8}> ${(aggregatedCost ?? totalCost).toFixed(2)} {hasSubtasks && ( * )}
{t("chat:costs.orchestrationLabel")} {t("chat:costs.orchestrationTotal", { cost: rootOrchestrationCost.toFixed(2), })} } side="top" sideOffset={8}> ${rootOrchestrationCost.toFixed(2)}
{t("chat:task.size")} {prettyBytes(currentTaskItem.size)}
)} {/* Todo list - always shown at bottom when todos exist */} {hasTodos && } ) } export default memo(TaskHeader)