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, Globe, 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 costBreakdown?: string contextTokens: number buttonsDisabled: boolean handleCondenseContext: (taskId: string) => void todos?: any[] } const TaskHeader = ({ task, tokensIn, tokensOut, cacheWrites, cacheReads, totalCost, aggregatedCost, hasSubtasks, parentTaskId, costBreakdown, contextTokens, buttonsDisabled, handleCondenseContext, todos, }: TaskHeaderProps) => { const { t } = useTranslation() const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive } = 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 // Detect if this task had any browser session activity so we can show a grey globe when inactive const browserSessionStartIndex = useMemo(() => { const msgs = clineMessages || [] for (let i = 0; i < msgs.length; i++) { const m = msgs[i] as any if (m?.ask === "browser_action_launch") return i } return -1 }, [clineMessages]) const showBrowserGlobe = browserSessionStartIndex !== -1 || !!isBrowserSessionActive 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 maxTokens = model ? getModelMaxOutputTokens({ modelId, model, settings: apiConfiguration, }) : 0 const reservedForOutput = maxTokens || 0 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}> {(() => { const percentage = Math.round(((contextTokens || 0) / contextWindow) * 100) 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 && ( * )} )}
{showBrowserGlobe && (
e.stopPropagation()}> {isBrowserSessionActive && ( {t("chat:browser.active")} )}
)}
)} {/* 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 && ( )} {/* 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:task.size")} {prettyBytes(currentTaskItem.size)}
)} {/* Todo list - always shown at bottom when todos exist */} {hasTodos && }
) } export default memo(TaskHeader)