TaskHeader.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { memo, useRef, useState } from "react"
  2. import { useWindowSize } from "react-use"
  3. import { useTranslation } from "react-i18next"
  4. import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
  5. import { CloudUpload, CloudDownload, FoldVertical } from "lucide-react"
  6. import type { ClineMessage } from "@roo-code/types"
  7. import { getModelMaxOutputTokens } from "@roo/api"
  8. import { formatLargeNumber } from "@src/utils/format"
  9. import { cn } from "@src/lib/utils"
  10. import { Button } from "@src/components/ui"
  11. import { useExtensionState } from "@src/context/ExtensionStateContext"
  12. import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
  13. import Thumbnails from "../common/Thumbnails"
  14. import { TaskActions } from "./TaskActions"
  15. import { ShareButton } from "./ShareButton"
  16. import { ContextWindowProgress } from "./ContextWindowProgress"
  17. import { Mention } from "./Mention"
  18. export interface TaskHeaderProps {
  19. task: ClineMessage
  20. tokensIn: number
  21. tokensOut: number
  22. doesModelSupportPromptCache: boolean
  23. cacheWrites?: number
  24. cacheReads?: number
  25. totalCost: number
  26. contextTokens: number
  27. buttonsDisabled: boolean
  28. handleCondenseContext: (taskId: string) => void
  29. onClose: () => void
  30. }
  31. const TaskHeader = ({
  32. task,
  33. tokensIn,
  34. tokensOut,
  35. doesModelSupportPromptCache,
  36. cacheWrites,
  37. cacheReads,
  38. totalCost,
  39. contextTokens,
  40. buttonsDisabled,
  41. handleCondenseContext,
  42. onClose,
  43. }: TaskHeaderProps) => {
  44. const { t } = useTranslation()
  45. const { apiConfiguration, currentTaskItem } = useExtensionState()
  46. const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
  47. const [isTaskExpanded, setIsTaskExpanded] = useState(false)
  48. const textContainerRef = useRef<HTMLDivElement>(null)
  49. const textRef = useRef<HTMLDivElement>(null)
  50. const contextWindow = model?.contextWindow || 1
  51. const { width: windowWidth } = useWindowSize()
  52. const condenseButton = (
  53. <button
  54. title={t("chat:task.condenseContext")}
  55. disabled={buttonsDisabled}
  56. onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
  57. className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer disabled:cursor-not-allowed opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
  58. <FoldVertical size={16} />
  59. </button>
  60. )
  61. return (
  62. <div className="py-2 px-3">
  63. <div
  64. className={cn(
  65. "rounded-xs p-2.5 flex flex-col gap-1.5 relative z-1 border",
  66. isTaskExpanded
  67. ? "border-vscode-panel-border text-vscode-foreground"
  68. : "border-vscode-panel-border/80 text-vscode-foreground/80",
  69. )}>
  70. <div className="flex justify-between items-center gap-2">
  71. <div
  72. className="flex items-center cursor-pointer -ml-0.5 select-none grow min-w-0"
  73. onClick={() => setIsTaskExpanded(!isTaskExpanded)}>
  74. <div className="flex items-center shrink-0">
  75. <span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
  76. </div>
  77. <div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
  78. <span className="font-bold">
  79. {t("chat:task.title")}
  80. {!isTaskExpanded && ":"}
  81. </span>
  82. {!isTaskExpanded && (
  83. <span className="ml-1">
  84. <Mention text={task.text} />
  85. </span>
  86. )}
  87. </div>
  88. </div>
  89. <Button
  90. variant="ghost"
  91. size="icon"
  92. onClick={onClose}
  93. title={t("chat:task.closeAndStart")}
  94. className="shrink-0 w-5 h-5">
  95. <span className="codicon codicon-close" />
  96. </Button>
  97. </div>
  98. {/* Collapsed state: Track context and cost if we have any */}
  99. {!isTaskExpanded && contextWindow > 0 && (
  100. <div className={`w-full flex flex-row items-center gap-1 h-auto`}>
  101. <ContextWindowProgress
  102. contextWindow={contextWindow}
  103. contextTokens={contextTokens || 0}
  104. maxTokens={
  105. model
  106. ? getModelMaxOutputTokens({ modelId, model, settings: apiConfiguration })
  107. : undefined
  108. }
  109. />
  110. {condenseButton}
  111. <ShareButton item={currentTaskItem} disabled={buttonsDisabled} />
  112. {!!totalCost && <VSCodeBadge>${totalCost.toFixed(2)}</VSCodeBadge>}
  113. </div>
  114. )}
  115. {/* Expanded state: Show task text and images */}
  116. {isTaskExpanded && (
  117. <>
  118. <div
  119. ref={textContainerRef}
  120. className="-mt-0.5 text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
  121. <div
  122. ref={textRef}
  123. className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere"
  124. style={{
  125. display: "-webkit-box",
  126. WebkitLineClamp: "unset",
  127. WebkitBoxOrient: "vertical",
  128. }}>
  129. <Mention text={task.text} />
  130. </div>
  131. </div>
  132. {task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
  133. <div className="flex flex-col gap-1">
  134. {isTaskExpanded && contextWindow > 0 && (
  135. <div
  136. className={`w-full flex ${windowWidth < 400 ? "flex-col" : "flex-row"} gap-1 h-auto`}>
  137. <div className="flex items-center gap-1 flex-shrink-0">
  138. <span className="font-bold" data-testid="context-window-label">
  139. {t("chat:task.contextWindow")}
  140. </span>
  141. </div>
  142. <ContextWindowProgress
  143. contextWindow={contextWindow}
  144. contextTokens={contextTokens || 0}
  145. maxTokens={
  146. model
  147. ? getModelMaxOutputTokens({
  148. modelId,
  149. model,
  150. settings: apiConfiguration,
  151. })
  152. : undefined
  153. }
  154. />
  155. {condenseButton}
  156. </div>
  157. )}
  158. <div className="flex justify-between items-center h-[20px]">
  159. <div className="flex items-center gap-1 flex-wrap">
  160. <span className="font-bold">{t("chat:task.tokens")}</span>
  161. {typeof tokensIn === "number" && tokensIn > 0 && (
  162. <span className="flex items-center gap-0.5">
  163. <i className="codicon codicon-arrow-up text-xs font-bold" />
  164. {formatLargeNumber(tokensIn)}
  165. </span>
  166. )}
  167. {typeof tokensOut === "number" && tokensOut > 0 && (
  168. <span className="flex items-center gap-0.5">
  169. <i className="codicon codicon-arrow-down text-xs font-bold" />
  170. {formatLargeNumber(tokensOut)}
  171. </span>
  172. )}
  173. </div>
  174. {!totalCost && <TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />}
  175. </div>
  176. {doesModelSupportPromptCache &&
  177. ((typeof cacheReads === "number" && cacheReads > 0) ||
  178. (typeof cacheWrites === "number" && cacheWrites > 0)) && (
  179. <div className="flex items-center gap-1 flex-wrap h-[20px]">
  180. <span className="font-bold">{t("chat:task.cache")}</span>
  181. {typeof cacheWrites === "number" && cacheWrites > 0 && (
  182. <span className="flex items-center gap-0.5">
  183. <CloudUpload size={16} />
  184. {formatLargeNumber(cacheWrites)}
  185. </span>
  186. )}
  187. {typeof cacheReads === "number" && cacheReads > 0 && (
  188. <span className="flex items-center gap-0.5">
  189. <CloudDownload size={16} />
  190. {formatLargeNumber(cacheReads)}
  191. </span>
  192. )}
  193. </div>
  194. )}
  195. {!!totalCost && (
  196. <div className="flex justify-between items-center h-[20px]">
  197. <div className="flex items-center gap-1">
  198. <span className="font-bold">{t("chat:task.apiCost")}</span>
  199. <span>${totalCost?.toFixed(2)}</span>
  200. </div>
  201. <TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />
  202. </div>
  203. )}
  204. </div>
  205. </>
  206. )}
  207. </div>
  208. </div>
  209. )
  210. }
  211. export default memo(TaskHeader)