ChatView.tsx 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
  2. import { useDeepCompareEffect, useEvent, useMount } from "react-use"
  3. import debounce from "debounce"
  4. import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
  5. import removeMd from "remove-markdown"
  6. import { Trans } from "react-i18next"
  7. import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
  8. import {
  9. ClineAsk,
  10. ClineMessage,
  11. ClineSayBrowserAction,
  12. ClineSayTool,
  13. ExtensionMessage,
  14. } from "@roo/shared/ExtensionMessage"
  15. import { McpServer, McpTool } from "@roo/shared/mcp"
  16. import { findLast } from "@roo/shared/array"
  17. import { combineApiRequests } from "@roo/shared/combineApiRequests"
  18. import { combineCommandSequences } from "@roo/shared/combineCommandSequences"
  19. import { getApiMetrics } from "@roo/shared/getApiMetrics"
  20. import { AudioType } from "@roo/shared/WebviewMessage"
  21. import { getAllModes } from "@roo/shared/modes"
  22. import { useExtensionState } from "@src/context/ExtensionStateContext"
  23. import { vscode } from "@src/utils/vscode"
  24. import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
  25. import { validateCommand } from "@src/utils/command-validation"
  26. import { useAppTranslation } from "@src/i18n/TranslationContext"
  27. import TelemetryBanner from "../common/TelemetryBanner"
  28. import { useTaskSearch } from "../history/useTaskSearch"
  29. import HistoryPreview from "../history/HistoryPreview"
  30. import RooHero from "@src/components/welcome/RooHero"
  31. import RooTips from "@src/components/welcome/RooTips"
  32. import Announcement from "./Announcement"
  33. import BrowserSessionRow from "./BrowserSessionRow"
  34. import ChatRow from "./ChatRow"
  35. import ChatTextArea from "./ChatTextArea"
  36. import TaskHeader from "./TaskHeader"
  37. import AutoApproveMenu from "./AutoApproveMenu"
  38. import SystemPromptWarning from "./SystemPromptWarning"
  39. import { CheckpointWarning } from "./CheckpointWarning"
  40. export interface ChatViewProps {
  41. isHidden: boolean
  42. showAnnouncement: boolean
  43. hideAnnouncement: () => void
  44. }
  45. export interface ChatViewRef {
  46. acceptInput: () => void
  47. }
  48. export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
  49. const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
  50. const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
  51. { isHidden, showAnnouncement, hideAnnouncement },
  52. ref,
  53. ) => {
  54. const { t } = useAppTranslation()
  55. const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}`
  56. const {
  57. clineMessages: messages,
  58. taskHistory,
  59. apiConfiguration,
  60. mcpServers,
  61. alwaysAllowBrowser,
  62. alwaysAllowReadOnly,
  63. alwaysAllowReadOnlyOutsideWorkspace,
  64. alwaysAllowWrite,
  65. alwaysAllowWriteOutsideWorkspace,
  66. alwaysAllowExecute,
  67. alwaysAllowMcp,
  68. allowedCommands,
  69. writeDelayMs,
  70. mode,
  71. setMode,
  72. autoApprovalEnabled,
  73. alwaysAllowModeSwitch,
  74. alwaysAllowSubtasks,
  75. customModes,
  76. telemetrySetting,
  77. hasSystemPromptOverride,
  78. historyPreviewCollapsed, // Added historyPreviewCollapsed
  79. } = useExtensionState()
  80. const { tasks } = useTaskSearch()
  81. // Initialize expanded state based on the persisted setting (default to expanded if undefined)
  82. const [isExpanded, setIsExpanded] = useState(
  83. historyPreviewCollapsed === undefined ? true : !historyPreviewCollapsed,
  84. )
  85. const toggleExpanded = useCallback(() => {
  86. const newState = !isExpanded
  87. setIsExpanded(newState)
  88. // Send message to extension to persist the new collapsed state
  89. vscode.postMessage({ type: "setHistoryPreviewCollapsed", bool: !newState })
  90. }, [isExpanded])
  91. // Leaving this less safe version here since if the first message is not a
  92. // task, then the extension is in a bad state and needs to be debugged (see
  93. // Cline.abort).
  94. const task = useMemo(() => messages.at(0), [messages])
  95. const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
  96. // Has to be after api_req_finished are all reduced into api_req_started messages.
  97. const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
  98. const [inputValue, setInputValue] = useState("")
  99. const textAreaRef = useRef<HTMLTextAreaElement>(null)
  100. const [textAreaDisabled, setTextAreaDisabled] = useState(false)
  101. const [selectedImages, setSelectedImages] = useState<string[]>([])
  102. // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
  103. const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
  104. const [enableButtons, setEnableButtons] = useState<boolean>(false)
  105. const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
  106. const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
  107. const [didClickCancel, setDidClickCancel] = useState(false)
  108. const virtuosoRef = useRef<VirtuosoHandle>(null)
  109. const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
  110. const scrollContainerRef = useRef<HTMLDivElement>(null)
  111. const disableAutoScrollRef = useRef(false)
  112. const [showScrollToBottom, setShowScrollToBottom] = useState(false)
  113. const [isAtBottom, setIsAtBottom] = useState(false)
  114. const lastTtsRef = useRef<string>("")
  115. const [wasStreaming, setWasStreaming] = useState<boolean>(false)
  116. const [showCheckpointWarning, setShowCheckpointWarning] = useState<boolean>(false)
  117. // UI layout depends on the last 2 messages
  118. // (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
  119. const lastMessage = useMemo(() => messages.at(-1), [messages])
  120. const secondLastMessage = useMemo(() => messages.at(-2), [messages])
  121. function playSound(audioType: AudioType) {
  122. vscode.postMessage({ type: "playSound", audioType })
  123. }
  124. function playTts(text: string) {
  125. vscode.postMessage({ type: "playTts", text })
  126. }
  127. useDeepCompareEffect(() => {
  128. // if last message is an ask, show user ask UI
  129. // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
  130. // basically as long as a task is active, the conversation history will be persisted
  131. if (lastMessage) {
  132. switch (lastMessage.type) {
  133. case "ask":
  134. const isPartial = lastMessage.partial === true
  135. switch (lastMessage.ask) {
  136. case "api_req_failed":
  137. playSound("progress_loop")
  138. setTextAreaDisabled(true)
  139. setClineAsk("api_req_failed")
  140. setEnableButtons(true)
  141. setPrimaryButtonText(t("chat:retry.title"))
  142. setSecondaryButtonText(t("chat:startNewTask.title"))
  143. break
  144. case "mistake_limit_reached":
  145. playSound("progress_loop")
  146. setTextAreaDisabled(false)
  147. setClineAsk("mistake_limit_reached")
  148. setEnableButtons(true)
  149. setPrimaryButtonText(t("chat:proceedAnyways.title"))
  150. setSecondaryButtonText(t("chat:startNewTask.title"))
  151. break
  152. case "followup":
  153. if (!isPartial) {
  154. playSound("notification")
  155. }
  156. setTextAreaDisabled(isPartial)
  157. setClineAsk("followup")
  158. // setting enable buttons to `false` would trigger a focus grab when
  159. // the text area is enabled which is undesirable.
  160. // We have no buttons for this tool, so no problem having them "enabled"
  161. // to workaround this issue. See #1358.
  162. setEnableButtons(true)
  163. setPrimaryButtonText(undefined)
  164. setSecondaryButtonText(undefined)
  165. break
  166. case "tool":
  167. if (!isAutoApproved(lastMessage) && !isPartial) {
  168. playSound("notification")
  169. }
  170. setTextAreaDisabled(isPartial)
  171. setClineAsk("tool")
  172. setEnableButtons(!isPartial)
  173. const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
  174. switch (tool.tool) {
  175. case "editedExistingFile":
  176. case "appliedDiff":
  177. case "newFileCreated":
  178. case "insertContent":
  179. setPrimaryButtonText(t("chat:save.title"))
  180. setSecondaryButtonText(t("chat:reject.title"))
  181. break
  182. case "finishTask":
  183. setPrimaryButtonText(t("chat:completeSubtaskAndReturn"))
  184. setSecondaryButtonText(undefined)
  185. break
  186. default:
  187. setPrimaryButtonText(t("chat:approve.title"))
  188. setSecondaryButtonText(t("chat:reject.title"))
  189. break
  190. }
  191. break
  192. case "browser_action_launch":
  193. if (!isAutoApproved(lastMessage) && !isPartial) {
  194. playSound("notification")
  195. }
  196. setTextAreaDisabled(isPartial)
  197. setClineAsk("browser_action_launch")
  198. setEnableButtons(!isPartial)
  199. setPrimaryButtonText(t("chat:approve.title"))
  200. setSecondaryButtonText(t("chat:reject.title"))
  201. break
  202. case "command":
  203. if (!isAutoApproved(lastMessage) && !isPartial) {
  204. playSound("notification")
  205. }
  206. setTextAreaDisabled(isPartial)
  207. setClineAsk("command")
  208. setEnableButtons(!isPartial)
  209. setPrimaryButtonText(t("chat:runCommand.title"))
  210. setSecondaryButtonText(t("chat:reject.title"))
  211. break
  212. case "command_output":
  213. setTextAreaDisabled(false)
  214. setClineAsk("command_output")
  215. setEnableButtons(true)
  216. setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
  217. setSecondaryButtonText(t("chat:killCommand.title"))
  218. break
  219. case "use_mcp_server":
  220. if (!isAutoApproved(lastMessage) && !isPartial) {
  221. playSound("notification")
  222. }
  223. setTextAreaDisabled(isPartial)
  224. setClineAsk("use_mcp_server")
  225. setEnableButtons(!isPartial)
  226. setPrimaryButtonText(t("chat:approve.title"))
  227. setSecondaryButtonText(t("chat:reject.title"))
  228. break
  229. case "completion_result":
  230. // extension waiting for feedback. but we can just present a new task button
  231. if (!isPartial) {
  232. playSound("celebration")
  233. }
  234. setTextAreaDisabled(isPartial)
  235. setClineAsk("completion_result")
  236. setEnableButtons(!isPartial)
  237. setPrimaryButtonText(t("chat:startNewTask.title"))
  238. setSecondaryButtonText(undefined)
  239. break
  240. case "resume_task":
  241. if (!isAutoApproved(lastMessage) && !isPartial) {
  242. playSound("notification")
  243. }
  244. setTextAreaDisabled(false)
  245. setClineAsk("resume_task")
  246. setEnableButtons(true)
  247. setPrimaryButtonText(t("chat:resumeTask.title"))
  248. setSecondaryButtonText(t("chat:terminate.title"))
  249. setDidClickCancel(false) // special case where we reset the cancel button state
  250. break
  251. case "resume_completed_task":
  252. if (!isPartial) {
  253. playSound("celebration")
  254. }
  255. setTextAreaDisabled(false)
  256. setClineAsk("resume_completed_task")
  257. setEnableButtons(true)
  258. setPrimaryButtonText(t("chat:startNewTask.title"))
  259. setSecondaryButtonText(undefined)
  260. setDidClickCancel(false)
  261. break
  262. }
  263. break
  264. case "say":
  265. // Don't want to reset since there could be a "say" after
  266. // an "ask" while ask is waiting for response.
  267. switch (lastMessage.say) {
  268. case "api_req_retry_delayed":
  269. setTextAreaDisabled(true)
  270. break
  271. case "api_req_started":
  272. if (secondLastMessage?.ask === "command_output") {
  273. // If the last ask is a command_output, and we
  274. // receive an api_req_started, then that means
  275. // the command has finished and we don't need
  276. // input from the user anymore (in every other
  277. // case, the user has to interact with input
  278. // field or buttons to continue, which does the
  279. // following automatically).
  280. setInputValue("")
  281. setTextAreaDisabled(true)
  282. setSelectedImages([])
  283. setClineAsk(undefined)
  284. setEnableButtons(false)
  285. }
  286. break
  287. case "api_req_finished":
  288. case "error":
  289. case "text":
  290. case "browser_action":
  291. case "browser_action_result":
  292. case "command_output":
  293. case "mcp_server_request_started":
  294. case "mcp_server_response":
  295. case "completion_result":
  296. break
  297. }
  298. break
  299. }
  300. }
  301. }, [lastMessage, secondLastMessage])
  302. useEffect(() => {
  303. if (messages.length === 0) {
  304. setTextAreaDisabled(false)
  305. setClineAsk(undefined)
  306. setEnableButtons(false)
  307. setPrimaryButtonText(undefined)
  308. setSecondaryButtonText(undefined)
  309. }
  310. }, [messages.length])
  311. useEffect(() => setExpandedRows({}), [task?.ts])
  312. const isStreaming = useMemo(() => {
  313. // Checking clineAsk isn't enough since messages effect may be called
  314. // again for a tool for example, set clineAsk to its value, and if the
  315. // next message is not an ask then it doesn't reset. This is likely due
  316. // to how much more often we're updating messages as compared to before,
  317. // and should be resolved with optimizations as it's likely a rendering
  318. // bug. But as a final guard for now, the cancel button will show if the
  319. // last message is not an ask.
  320. const isLastAsk = !!modifiedMessages.at(-1)?.ask
  321. const isToolCurrentlyAsking =
  322. isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
  323. if (isToolCurrentlyAsking) {
  324. return false
  325. }
  326. const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
  327. if (isLastMessagePartial) {
  328. return true
  329. } else {
  330. const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
  331. if (
  332. lastApiReqStarted &&
  333. lastApiReqStarted.text !== null &&
  334. lastApiReqStarted.text !== undefined &&
  335. lastApiReqStarted.say === "api_req_started"
  336. ) {
  337. const cost = JSON.parse(lastApiReqStarted.text).cost
  338. if (cost === undefined) {
  339. return true // API request has not finished yet.
  340. }
  341. }
  342. }
  343. return false
  344. }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
  345. const handleChatReset = useCallback(() => {
  346. // Only reset message-specific state, preserving mode.
  347. setInputValue("")
  348. setTextAreaDisabled(true)
  349. setSelectedImages([])
  350. setClineAsk(undefined)
  351. setEnableButtons(false)
  352. // Do not reset mode here as it should persist.
  353. // setPrimaryButtonText(undefined)
  354. // setSecondaryButtonText(undefined)
  355. disableAutoScrollRef.current = false
  356. }, [])
  357. const handleSendMessage = useCallback(
  358. (text: string, images: string[]) => {
  359. text = text.trim()
  360. if (text || images.length > 0) {
  361. if (messages.length === 0) {
  362. vscode.postMessage({ type: "newTask", text, images })
  363. } else if (clineAsk) {
  364. switch (clineAsk) {
  365. case "followup":
  366. case "tool":
  367. case "browser_action_launch":
  368. case "command": // User can provide feedback to a tool or command use.
  369. case "command_output": // User can send input to command stdin.
  370. case "use_mcp_server":
  371. case "completion_result": // If this happens then the user has feedback for the completion result.
  372. case "resume_task":
  373. case "resume_completed_task":
  374. case "mistake_limit_reached":
  375. vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
  376. break
  377. // There is no other case that a textfield should be enabled.
  378. }
  379. }
  380. handleChatReset()
  381. }
  382. },
  383. [messages.length, clineAsk, handleChatReset],
  384. )
  385. const handleSetChatBoxMessage = useCallback(
  386. (text: string, images: string[]) => {
  387. // Avoid nested template literals by breaking down the logic
  388. let newValue = text
  389. if (inputValue !== "") {
  390. newValue = inputValue + " " + text
  391. }
  392. setInputValue(newValue)
  393. setSelectedImages([...selectedImages, ...images])
  394. },
  395. [inputValue, selectedImages],
  396. )
  397. const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
  398. // This logic depends on the useEffect[messages] above to set clineAsk,
  399. // after which buttons are shown and we then send an askResponse to the
  400. // extension.
  401. const handlePrimaryButtonClick = useCallback(
  402. (text?: string, images?: string[]) => {
  403. const trimmedInput = text?.trim()
  404. switch (clineAsk) {
  405. case "api_req_failed":
  406. case "command":
  407. case "tool":
  408. case "browser_action_launch":
  409. case "use_mcp_server":
  410. case "resume_task":
  411. case "mistake_limit_reached":
  412. // Only send text/images if they exist
  413. if (trimmedInput || (images && images.length > 0)) {
  414. vscode.postMessage({
  415. type: "askResponse",
  416. askResponse: "yesButtonClicked",
  417. text: trimmedInput,
  418. images: images,
  419. })
  420. } else {
  421. vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
  422. }
  423. // Clear input state after sending
  424. setInputValue("")
  425. setSelectedImages([])
  426. break
  427. case "completion_result":
  428. case "resume_completed_task":
  429. // Waiting for feedback, but we can just present a new task button
  430. startNewTask()
  431. break
  432. case "command_output":
  433. vscode.postMessage({ type: "terminalOperation", terminalOperation: "continue" })
  434. break
  435. }
  436. setTextAreaDisabled(true)
  437. setClineAsk(undefined)
  438. setEnableButtons(false)
  439. },
  440. [clineAsk, startNewTask],
  441. )
  442. const handleSecondaryButtonClick = useCallback(
  443. (text?: string, images?: string[]) => {
  444. const trimmedInput = text?.trim()
  445. if (isStreaming) {
  446. vscode.postMessage({ type: "cancelTask" })
  447. setDidClickCancel(true)
  448. return
  449. }
  450. switch (clineAsk) {
  451. case "api_req_failed":
  452. case "mistake_limit_reached":
  453. case "resume_task":
  454. startNewTask()
  455. break
  456. case "command":
  457. case "tool":
  458. case "browser_action_launch":
  459. case "use_mcp_server":
  460. // Only send text/images if they exist
  461. if (trimmedInput || (images && images.length > 0)) {
  462. vscode.postMessage({
  463. type: "askResponse",
  464. askResponse: "noButtonClicked",
  465. text: trimmedInput,
  466. images: images,
  467. })
  468. } else {
  469. // Responds to the API with a "This operation failed" and lets it try again
  470. vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
  471. }
  472. // Clear input state after sending
  473. setInputValue("")
  474. setSelectedImages([])
  475. break
  476. case "command_output":
  477. vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
  478. break
  479. }
  480. setTextAreaDisabled(true)
  481. setClineAsk(undefined)
  482. setEnableButtons(false)
  483. },
  484. [clineAsk, startNewTask, isStreaming],
  485. )
  486. const handleTaskCloseButtonClick = useCallback(() => startNewTask(), [startNewTask])
  487. const { info: model } = useSelectedModel(apiConfiguration)
  488. const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
  489. const shouldDisableImages =
  490. !model?.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
  491. const handleMessage = useCallback(
  492. (e: MessageEvent) => {
  493. const message: ExtensionMessage = e.data
  494. switch (message.type) {
  495. case "action":
  496. switch (message.action!) {
  497. case "didBecomeVisible":
  498. if (!isHidden && !textAreaDisabled && !enableButtons) {
  499. textAreaRef.current?.focus()
  500. }
  501. break
  502. case "focusInput":
  503. textAreaRef.current?.focus()
  504. break
  505. }
  506. break
  507. case "selectedImages":
  508. const newImages = message.images ?? []
  509. if (newImages.length > 0) {
  510. setSelectedImages((prevImages) =>
  511. [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
  512. )
  513. }
  514. break
  515. case "invoke":
  516. switch (message.invoke!) {
  517. case "newChat":
  518. handleChatReset()
  519. break
  520. case "sendMessage":
  521. handleSendMessage(message.text ?? "", message.images ?? [])
  522. break
  523. case "setChatBoxMessage":
  524. handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
  525. break
  526. case "primaryButtonClick":
  527. handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
  528. break
  529. case "secondaryButtonClick":
  530. handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
  531. break
  532. }
  533. }
  534. // textAreaRef.current is not explicitly required here since React
  535. // guarantees that ref will be stable across re-renders, and we're
  536. // not using its value but its reference.
  537. },
  538. [
  539. isHidden,
  540. textAreaDisabled,
  541. enableButtons,
  542. handleChatReset,
  543. handleSendMessage,
  544. handleSetChatBoxMessage,
  545. handlePrimaryButtonClick,
  546. handleSecondaryButtonClick,
  547. ],
  548. )
  549. useEvent("message", handleMessage)
  550. // NOTE: the VSCode window needs to be focused for this to work.
  551. useMount(() => textAreaRef.current?.focus())
  552. useEffect(() => {
  553. const timer = setTimeout(() => {
  554. if (!isHidden && !textAreaDisabled && !enableButtons) {
  555. textAreaRef.current?.focus()
  556. }
  557. }, 50)
  558. return () => {
  559. clearTimeout(timer)
  560. }
  561. }, [isHidden, textAreaDisabled, enableButtons])
  562. const visibleMessages = useMemo(() => {
  563. return modifiedMessages.filter((message) => {
  564. switch (message.ask) {
  565. case "completion_result":
  566. // Don't show a chat row for a completion_result ask without
  567. // text. This specific type of message only occurs if cline
  568. // wants to execute a command as part of its completion
  569. // result, in which case we interject the completion_result
  570. // tool with the execute_command tool.
  571. if (message.text === "") {
  572. return false
  573. }
  574. break
  575. case "api_req_failed": // This message is used to update the latest `api_req_started` that the request failed.
  576. case "resume_task":
  577. case "resume_completed_task":
  578. return false
  579. }
  580. switch (message.say) {
  581. case "api_req_finished": // `combineApiRequests` removes this from `modifiedMessages` anyways.
  582. case "api_req_retried": // This message is used to update the latest `api_req_started` that the request was retried.
  583. case "api_req_deleted": // Aggregated `api_req` metrics from deleted messages.
  584. return false
  585. case "api_req_retry_delayed":
  586. // Only show the retry message if it's the last message or
  587. // the last messages is api_req_retry_delayed+resume_task.
  588. const last1 = modifiedMessages.at(-1)
  589. const last2 = modifiedMessages.at(-2)
  590. if (last1?.ask === "resume_task" && last2 === message) {
  591. return true
  592. }
  593. return message === last1
  594. case "text":
  595. // Sometimes cline returns an empty text message, we don't
  596. // want to render these. (We also use a say text for user
  597. // messages, so in case they just sent images we still
  598. // render that.)
  599. if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
  600. return false
  601. }
  602. break
  603. case "mcp_server_request_started":
  604. return false
  605. }
  606. return true
  607. })
  608. }, [modifiedMessages])
  609. const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => {
  610. if (message?.type === "ask") {
  611. if (!message.text) {
  612. return true
  613. }
  614. const tool = JSON.parse(message.text)
  615. return [
  616. "readFile",
  617. "listFiles",
  618. "listFilesTopLevel",
  619. "listFilesRecursive",
  620. "listCodeDefinitionNames",
  621. "searchFiles",
  622. ].includes(tool.tool)
  623. }
  624. return false
  625. }, [])
  626. const isWriteToolAction = useCallback((message: ClineMessage | undefined) => {
  627. if (message?.type === "ask") {
  628. if (!message.text) {
  629. return true
  630. }
  631. const tool = JSON.parse(message.text)
  632. return [
  633. "editedExistingFile",
  634. "appliedDiff",
  635. "newFileCreated",
  636. "searchAndReplace",
  637. "insertContent",
  638. ].includes(tool.tool)
  639. }
  640. return false
  641. }, [])
  642. const isMcpToolAlwaysAllowed = useCallback(
  643. (message: ClineMessage | undefined) => {
  644. if (message?.type === "ask" && message.ask === "use_mcp_server") {
  645. if (!message.text) {
  646. return true
  647. }
  648. const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
  649. if (mcpServerUse.type === "use_mcp_tool") {
  650. const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
  651. const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
  652. return tool?.alwaysAllow || false
  653. }
  654. }
  655. return false
  656. },
  657. [mcpServers],
  658. )
  659. // Check if a command message is allowed.
  660. const isAllowedCommand = useCallback(
  661. (message: ClineMessage | undefined): boolean => {
  662. if (message?.type !== "ask") return false
  663. return validateCommand(message.text || "", allowedCommands || [])
  664. },
  665. [allowedCommands],
  666. )
  667. const isAutoApproved = useCallback(
  668. (message: ClineMessage | undefined) => {
  669. if (!autoApprovalEnabled || !message || message.type !== "ask") return false
  670. if (message.ask === "browser_action_launch") {
  671. return alwaysAllowBrowser
  672. }
  673. if (message.ask === "use_mcp_server") {
  674. return alwaysAllowMcp && isMcpToolAlwaysAllowed(message)
  675. }
  676. if (message.ask === "command") {
  677. return alwaysAllowExecute && isAllowedCommand(message)
  678. }
  679. // For read/write operations, check if it's outside workspace and if we have permission for that
  680. if (message.ask === "tool") {
  681. let tool: any = {}
  682. try {
  683. tool = JSON.parse(message.text || "{}")
  684. } catch (error) {
  685. console.error("Failed to parse tool:", error)
  686. }
  687. if (!tool) {
  688. return false
  689. }
  690. if (tool?.tool === "fetchInstructions") {
  691. if (tool.content === "create_mode") {
  692. return alwaysAllowModeSwitch
  693. }
  694. if (tool.content === "create_mcp_server") {
  695. return alwaysAllowMcp
  696. }
  697. }
  698. if (tool?.tool === "switchMode") {
  699. return alwaysAllowModeSwitch
  700. }
  701. if (["newTask", "finishTask"].includes(tool?.tool)) {
  702. return alwaysAllowSubtasks
  703. }
  704. const isOutsideWorkspace = !!tool.isOutsideWorkspace
  705. if (isReadOnlyToolAction(message)) {
  706. return alwaysAllowReadOnly && (!isOutsideWorkspace || alwaysAllowReadOnlyOutsideWorkspace)
  707. }
  708. if (isWriteToolAction(message)) {
  709. return alwaysAllowWrite && (!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace)
  710. }
  711. }
  712. return false
  713. },
  714. [
  715. autoApprovalEnabled,
  716. alwaysAllowBrowser,
  717. alwaysAllowReadOnly,
  718. alwaysAllowReadOnlyOutsideWorkspace,
  719. isReadOnlyToolAction,
  720. alwaysAllowWrite,
  721. alwaysAllowWriteOutsideWorkspace,
  722. isWriteToolAction,
  723. alwaysAllowExecute,
  724. isAllowedCommand,
  725. alwaysAllowMcp,
  726. isMcpToolAlwaysAllowed,
  727. alwaysAllowModeSwitch,
  728. alwaysAllowSubtasks,
  729. ],
  730. )
  731. useEffect(() => {
  732. // This ensures the first message is not read, future user messages are
  733. // labeled as `user_feedback`.
  734. if (lastMessage && messages.length > 1) {
  735. if (
  736. lastMessage.text && // has text
  737. (lastMessage.say === "text" || lastMessage.say === "completion_result") && // is a text message
  738. !lastMessage.partial && // not a partial message
  739. !lastMessage.text.startsWith("{") // not a json object
  740. ) {
  741. let text = lastMessage?.text || ""
  742. const mermaidRegex = /```mermaid[\s\S]*?```/g
  743. // remove mermaid diagrams from text
  744. text = text.replace(mermaidRegex, "")
  745. // remove markdown from text
  746. text = removeMd(text)
  747. // ensure message is not a duplicate of last read message
  748. if (text !== lastTtsRef.current) {
  749. try {
  750. playTts(text)
  751. lastTtsRef.current = text
  752. } catch (error) {
  753. console.error("Failed to execute text-to-speech:", error)
  754. }
  755. }
  756. }
  757. }
  758. // Update previous value.
  759. setWasStreaming(isStreaming)
  760. }, [isStreaming, lastMessage, wasStreaming, isAutoApproved, messages.length])
  761. const isBrowserSessionMessage = (message: ClineMessage): boolean => {
  762. // Which of visible messages are browser session messages, see above.
  763. if (message.type === "ask") {
  764. return ["browser_action_launch"].includes(message.ask!)
  765. }
  766. if (message.type === "say") {
  767. return ["api_req_started", "text", "browser_action", "browser_action_result"].includes(message.say!)
  768. }
  769. return false
  770. }
  771. const groupedMessages = useMemo(() => {
  772. const result: (ClineMessage | ClineMessage[])[] = []
  773. let currentGroup: ClineMessage[] = []
  774. let isInBrowserSession = false
  775. const endBrowserSession = () => {
  776. if (currentGroup.length > 0) {
  777. result.push([...currentGroup])
  778. currentGroup = []
  779. isInBrowserSession = false
  780. }
  781. }
  782. visibleMessages.forEach((message) => {
  783. if (message.ask === "browser_action_launch") {
  784. // Complete existing browser session if any.
  785. endBrowserSession()
  786. // Start new.
  787. isInBrowserSession = true
  788. currentGroup.push(message)
  789. } else if (isInBrowserSession) {
  790. // End session if `api_req_started` is cancelled.
  791. if (message.say === "api_req_started") {
  792. // Get last `api_req_started` in currentGroup to check if
  793. // it's cancelled. If it is then this api req is not part
  794. // of the current browser session.
  795. const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
  796. if (lastApiReqStarted?.text !== null && lastApiReqStarted?.text !== undefined) {
  797. const info = JSON.parse(lastApiReqStarted.text)
  798. const isCancelled = info.cancelReason !== null && info.cancelReason !== undefined
  799. if (isCancelled) {
  800. endBrowserSession()
  801. result.push(message)
  802. return
  803. }
  804. }
  805. }
  806. if (isBrowserSessionMessage(message)) {
  807. currentGroup.push(message)
  808. // Check if this is a close action
  809. if (message.say === "browser_action") {
  810. const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
  811. if (browserAction.action === "close") {
  812. endBrowserSession()
  813. }
  814. }
  815. } else {
  816. // complete existing browser session if any
  817. endBrowserSession()
  818. result.push(message)
  819. }
  820. } else {
  821. result.push(message)
  822. }
  823. })
  824. // Handle case where browser session is the last group
  825. if (currentGroup.length > 0) {
  826. result.push([...currentGroup])
  827. }
  828. return result
  829. }, [visibleMessages])
  830. // scrolling
  831. const scrollToBottomSmooth = useMemo(
  832. () =>
  833. debounce(() => virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" }), 10, {
  834. immediate: true,
  835. }),
  836. [],
  837. )
  838. const scrollToBottomAuto = useCallback(() => {
  839. virtuosoRef.current?.scrollTo({
  840. top: Number.MAX_SAFE_INTEGER,
  841. behavior: "auto", // Instant causes crash.
  842. })
  843. }, [])
  844. // Scroll when user toggles certain rows.
  845. const toggleRowExpansion = useCallback(
  846. (ts: number) => {
  847. const isCollapsing = expandedRows[ts] ?? false
  848. const lastGroup = groupedMessages.at(-1)
  849. const isLast = Array.isArray(lastGroup) ? lastGroup[0].ts === ts : lastGroup?.ts === ts
  850. const secondToLastGroup = groupedMessages.at(-2)
  851. const isSecondToLast = Array.isArray(secondToLastGroup)
  852. ? secondToLastGroup[0].ts === ts
  853. : secondToLastGroup?.ts === ts
  854. const isLastCollapsedApiReq =
  855. isLast &&
  856. !Array.isArray(lastGroup) && // Make sure it's not a browser session group
  857. lastGroup?.say === "api_req_started" &&
  858. !expandedRows[lastGroup.ts]
  859. setExpandedRows((prev) => ({ ...prev, [ts]: !prev[ts] }))
  860. // Disable auto scroll when user expands row
  861. if (!isCollapsing) {
  862. disableAutoScrollRef.current = true
  863. }
  864. if (isCollapsing && isAtBottom) {
  865. const timer = setTimeout(() => scrollToBottomAuto(), 0)
  866. return () => clearTimeout(timer)
  867. } else if (isLast || isSecondToLast) {
  868. if (isCollapsing) {
  869. if (isSecondToLast && !isLastCollapsedApiReq) {
  870. return
  871. }
  872. const timer = setTimeout(() => scrollToBottomAuto(), 0)
  873. return () => clearTimeout(timer)
  874. } else {
  875. const timer = setTimeout(() => {
  876. virtuosoRef.current?.scrollToIndex({
  877. index: groupedMessages.length - (isLast ? 1 : 2),
  878. align: "start",
  879. })
  880. }, 0)
  881. return () => clearTimeout(timer)
  882. }
  883. }
  884. },
  885. [groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom],
  886. )
  887. const handleRowHeightChange = useCallback(
  888. (isTaller: boolean) => {
  889. if (!disableAutoScrollRef.current) {
  890. if (isTaller) {
  891. scrollToBottomSmooth()
  892. } else {
  893. setTimeout(() => scrollToBottomAuto(), 0)
  894. }
  895. }
  896. },
  897. [scrollToBottomSmooth, scrollToBottomAuto],
  898. )
  899. useEffect(() => {
  900. if (!disableAutoScrollRef.current) {
  901. setTimeout(() => scrollToBottomSmooth(), 50)
  902. // Don't cleanup since if visibleMessages.length changes it cancels.
  903. // return () => clearTimeout(timer)
  904. }
  905. }, [groupedMessages.length, scrollToBottomSmooth])
  906. const handleWheel = useCallback((event: Event) => {
  907. const wheelEvent = event as WheelEvent
  908. if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
  909. if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
  910. // User scrolled up
  911. disableAutoScrollRef.current = true
  912. }
  913. }
  914. }, [])
  915. useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
  916. // Effect to handle showing the checkpoint warning after a delay
  917. useEffect(() => {
  918. // Only show the warning when there's a task but no visible messages yet
  919. if (task && modifiedMessages.length === 0 && !isStreaming) {
  920. const timer = setTimeout(() => {
  921. setShowCheckpointWarning(true)
  922. }, 5000) // 5 seconds
  923. return () => clearTimeout(timer)
  924. }
  925. }, [task, modifiedMessages.length, isStreaming])
  926. // Effect to hide the checkpoint warning when messages appear
  927. useEffect(() => {
  928. if (modifiedMessages.length > 0 || isStreaming) {
  929. setShowCheckpointWarning(false)
  930. }
  931. }, [modifiedMessages.length, isStreaming])
  932. const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask")
  933. const itemContent = useCallback(
  934. (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => {
  935. // browser session group
  936. if (Array.isArray(messageOrGroup)) {
  937. return (
  938. <BrowserSessionRow
  939. messages={messageOrGroup}
  940. isLast={index === groupedMessages.length - 1}
  941. lastModifiedMessage={modifiedMessages.at(-1)}
  942. onHeightChange={handleRowHeightChange}
  943. isStreaming={isStreaming}
  944. // Pass handlers for each message in the group
  945. isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
  946. onToggleExpand={(messageTs: number) => {
  947. setExpandedRows((prev) => ({
  948. ...prev,
  949. [messageTs]: !prev[messageTs],
  950. }))
  951. }}
  952. />
  953. )
  954. }
  955. // regular message
  956. return (
  957. <ChatRow
  958. key={messageOrGroup.ts}
  959. message={messageOrGroup}
  960. isExpanded={expandedRows[messageOrGroup.ts] || false}
  961. onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
  962. lastModifiedMessage={modifiedMessages.at(-1)}
  963. isLast={index === groupedMessages.length - 1}
  964. onHeightChange={handleRowHeightChange}
  965. isStreaming={isStreaming}
  966. onSuggestionClick={(answer: string, event?: React.MouseEvent) => {
  967. if (event?.shiftKey) {
  968. // Always append to existing text, don't overwrite
  969. setInputValue((currentValue) => {
  970. return currentValue !== "" ? `${currentValue} \n${answer}` : answer
  971. })
  972. } else {
  973. handleSendMessage(answer, [])
  974. }
  975. }}
  976. />
  977. )
  978. },
  979. [
  980. expandedRows,
  981. modifiedMessages,
  982. groupedMessages.length,
  983. handleRowHeightChange,
  984. isStreaming,
  985. toggleRowExpansion,
  986. handleSendMessage,
  987. ],
  988. )
  989. useEffect(() => {
  990. // Only proceed if we have an ask and buttons are enabled.
  991. if (!clineAsk || !enableButtons) {
  992. return
  993. }
  994. const autoApprove = async () => {
  995. if (isAutoApproved(lastMessage)) {
  996. // Add delay for write operations.
  997. if (lastMessage?.ask === "tool" && isWriteToolAction(lastMessage)) {
  998. await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
  999. }
  1000. handlePrimaryButtonClick()
  1001. }
  1002. }
  1003. autoApprove()
  1004. }, [
  1005. clineAsk,
  1006. enableButtons,
  1007. handlePrimaryButtonClick,
  1008. alwaysAllowBrowser,
  1009. alwaysAllowReadOnly,
  1010. alwaysAllowReadOnlyOutsideWorkspace,
  1011. alwaysAllowWrite,
  1012. alwaysAllowWriteOutsideWorkspace,
  1013. alwaysAllowExecute,
  1014. alwaysAllowMcp,
  1015. messages,
  1016. allowedCommands,
  1017. mcpServers,
  1018. isAutoApproved,
  1019. lastMessage,
  1020. writeDelayMs,
  1021. isWriteToolAction,
  1022. ])
  1023. // Function to handle mode switching
  1024. const switchToNextMode = useCallback(() => {
  1025. const allModes = getAllModes(customModes)
  1026. const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
  1027. const nextModeIndex = (currentModeIndex + 1) % allModes.length
  1028. // Update local state and notify extension to sync mode change
  1029. setMode(allModes[nextModeIndex].slug)
  1030. vscode.postMessage({
  1031. type: "mode",
  1032. text: allModes[nextModeIndex].slug,
  1033. })
  1034. }, [mode, setMode, customModes])
  1035. // Add keyboard event handler
  1036. const handleKeyDown = useCallback(
  1037. (event: KeyboardEvent) => {
  1038. // Check for Command + . (period)
  1039. if ((event.metaKey || event.ctrlKey) && event.key === ".") {
  1040. event.preventDefault() // Prevent default browser behavior
  1041. switchToNextMode()
  1042. }
  1043. },
  1044. [switchToNextMode],
  1045. )
  1046. // Add event listener
  1047. useEffect(() => {
  1048. window.addEventListener("keydown", handleKeyDown)
  1049. return () => {
  1050. window.removeEventListener("keydown", handleKeyDown)
  1051. }
  1052. }, [handleKeyDown])
  1053. useImperativeHandle(ref, () => ({
  1054. acceptInput: () => {
  1055. if (enableButtons && primaryButtonText) {
  1056. handlePrimaryButtonClick(inputValue, selectedImages)
  1057. } else if (!textAreaDisabled && (inputValue.trim() || selectedImages.length > 0)) {
  1058. handleSendMessage(inputValue, selectedImages)
  1059. }
  1060. },
  1061. }))
  1062. return (
  1063. <div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
  1064. {showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} />}
  1065. {task ? (
  1066. <>
  1067. <TaskHeader
  1068. task={task}
  1069. tokensIn={apiMetrics.totalTokensIn}
  1070. tokensOut={apiMetrics.totalTokensOut}
  1071. doesModelSupportPromptCache={model?.supportsPromptCache ?? false}
  1072. cacheWrites={apiMetrics.totalCacheWrites}
  1073. cacheReads={apiMetrics.totalCacheReads}
  1074. totalCost={apiMetrics.totalCost}
  1075. contextTokens={apiMetrics.contextTokens}
  1076. onClose={handleTaskCloseButtonClick}
  1077. />
  1078. {hasSystemPromptOverride && (
  1079. <div className="px-3">
  1080. <SystemPromptWarning />
  1081. </div>
  1082. )}
  1083. {showCheckpointWarning && (
  1084. <div className="px-3">
  1085. <CheckpointWarning />
  1086. </div>
  1087. )}
  1088. </>
  1089. ) : (
  1090. <div className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-4">
  1091. {/* Moved Task Bar Header Here */}
  1092. {tasks.length !== 0 && (
  1093. <div className="flex text-vscode-descriptionForeground w-full mx-auto px-5 pt-3">
  1094. <div className="flex items-center gap-1 cursor-pointer" onClick={toggleExpanded}>
  1095. {tasks.length < 10 && (
  1096. <span className={`font-medium text-xs `}>{t("history:recentTasks")}</span>
  1097. )}
  1098. <span
  1099. className={`codicon ${isExpanded ? "codicon-eye" : "codicon-eye-closed"} scale-90`}
  1100. />
  1101. </div>
  1102. </div>
  1103. )}
  1104. <div
  1105. className={` w-full flex flex-col gap-4 m-auto ${isExpanded && tasks.length > 0 ? "mt-0" : ""} px-3.5 min-[370px]:px-10 pt-5 transition-all duration-300`}>
  1106. <RooHero />
  1107. {telemetrySetting === "unset" && <TelemetryBanner />}
  1108. {/* Show the task history preview if expanded and tasks exist */}
  1109. {taskHistory.length > 0 && isExpanded && <HistoryPreview />}
  1110. <p className="ext-vscode-editor-foreground leading-tight font-vscode text-center">
  1111. <Trans
  1112. i18nKey="chat:about"
  1113. components={{
  1114. DocsLink: (
  1115. <a href="https://docs.roocode.com/" target="_blank" rel="noopener noreferrer">
  1116. the docs
  1117. </a>
  1118. ),
  1119. }}
  1120. />
  1121. </p>
  1122. <RooTips cycle={false} />
  1123. </div>
  1124. </div>
  1125. )}
  1126. {/*
  1127. // Flex layout explanation:
  1128. // 1. Content div above uses flex: "1 1 0" to:
  1129. // - Grow to fill available space (flex-grow: 1)
  1130. // - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
  1131. // - Start from zero size (flex-basis: 0) to ensure proper distribution
  1132. // minHeight: 0 allows it to shrink below its content height
  1133. //
  1134. // 2. AutoApproveMenu uses flex: "0 1 auto" to:
  1135. // - Not grow beyond its content (flex-grow: 0)
  1136. // - Shrink when viewport is small (flex-shrink: 1)
  1137. // - Use its content size as basis (flex-basis: auto)
  1138. // This ensures it takes its natural height when there's space
  1139. // but becomes scrollable when the viewport is too small
  1140. */}
  1141. {!task && (
  1142. <div className="mb-[-2px] flex-initial min-h-0">
  1143. <AutoApproveMenu />
  1144. </div>
  1145. )}
  1146. {task && (
  1147. <>
  1148. <div className="grow flex" ref={scrollContainerRef}>
  1149. <Virtuoso
  1150. ref={virtuosoRef}
  1151. key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
  1152. className="scrollable grow overflow-y-scroll"
  1153. components={{
  1154. Footer: () => <div className="h-[5px]" />, // Add empty padding at the bottom
  1155. }}
  1156. // increasing top by 3_000 to prevent jumping around when user collapses a row
  1157. increaseViewportBy={{ top: 3_000, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
  1158. data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
  1159. itemContent={itemContent}
  1160. atBottomStateChange={(isAtBottom) => {
  1161. setIsAtBottom(isAtBottom)
  1162. if (isAtBottom) {
  1163. disableAutoScrollRef.current = false
  1164. }
  1165. setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
  1166. }}
  1167. atBottomThreshold={10} // anything lower causes issues with followOutput
  1168. initialTopMostItemIndex={groupedMessages.length - 1}
  1169. />
  1170. </div>
  1171. <AutoApproveMenu />
  1172. {showScrollToBottom ? (
  1173. <div className="flex px-[15px] pt-[10px]">
  1174. <div
  1175. className="bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_55%,_transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_90%,_transparent)] active:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_70%,_transparent)]"
  1176. onClick={() => {
  1177. scrollToBottomSmooth()
  1178. disableAutoScrollRef.current = false
  1179. }}
  1180. title={t("chat:scrollToBottom")}>
  1181. <span className="codicon codicon-chevron-down text-[18px]"></span>
  1182. </div>
  1183. </div>
  1184. ) : (
  1185. <div
  1186. className={`flex ${
  1187. primaryButtonText || secondaryButtonText || isStreaming ? "px-[15px] pt-[10px]" : "p-0"
  1188. } ${
  1189. primaryButtonText || secondaryButtonText || isStreaming
  1190. ? enableButtons || (isStreaming && !didClickCancel)
  1191. ? "opacity-100"
  1192. : "opacity-50"
  1193. : "opacity-0"
  1194. }`}>
  1195. {primaryButtonText && !isStreaming && (
  1196. <VSCodeButton
  1197. appearance="primary"
  1198. disabled={!enableButtons}
  1199. className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
  1200. title={
  1201. primaryButtonText === t("chat:retry.title")
  1202. ? t("chat:retry.tooltip")
  1203. : primaryButtonText === t("chat:save.title")
  1204. ? t("chat:save.tooltip")
  1205. : primaryButtonText === t("chat:approve.title")
  1206. ? t("chat:approve.tooltip")
  1207. : primaryButtonText === t("chat:runCommand.title")
  1208. ? t("chat:runCommand.tooltip")
  1209. : primaryButtonText === t("chat:startNewTask.title")
  1210. ? t("chat:startNewTask.tooltip")
  1211. : primaryButtonText === t("chat:resumeTask.title")
  1212. ? t("chat:resumeTask.tooltip")
  1213. : primaryButtonText === t("chat:proceedAnyways.title")
  1214. ? t("chat:proceedAnyways.tooltip")
  1215. : primaryButtonText ===
  1216. t("chat:proceedWhileRunning.title")
  1217. ? t("chat:proceedWhileRunning.tooltip")
  1218. : undefined
  1219. }
  1220. onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
  1221. {primaryButtonText}
  1222. </VSCodeButton>
  1223. )}
  1224. {(secondaryButtonText || isStreaming) && (
  1225. <VSCodeButton
  1226. appearance="secondary"
  1227. disabled={!enableButtons && !(isStreaming && !didClickCancel)}
  1228. className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
  1229. title={
  1230. isStreaming
  1231. ? t("chat:cancel.tooltip")
  1232. : secondaryButtonText === t("chat:startNewTask.title")
  1233. ? t("chat:startNewTask.tooltip")
  1234. : secondaryButtonText === t("chat:reject.title")
  1235. ? t("chat:reject.tooltip")
  1236. : secondaryButtonText === t("chat:terminate.title")
  1237. ? t("chat:terminate.tooltip")
  1238. : undefined
  1239. }
  1240. onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
  1241. {isStreaming ? t("chat:cancel.title") : secondaryButtonText}
  1242. </VSCodeButton>
  1243. )}
  1244. </div>
  1245. )}
  1246. </>
  1247. )}
  1248. <ChatTextArea
  1249. ref={textAreaRef}
  1250. inputValue={inputValue}
  1251. setInputValue={setInputValue}
  1252. textAreaDisabled={textAreaDisabled}
  1253. selectApiConfigDisabled={textAreaDisabled && clineAsk !== "api_req_failed"}
  1254. placeholderText={placeholderText}
  1255. selectedImages={selectedImages}
  1256. setSelectedImages={setSelectedImages}
  1257. onSend={() => handleSendMessage(inputValue, selectedImages)}
  1258. onSelectImages={selectImages}
  1259. shouldDisableImages={shouldDisableImages}
  1260. onHeightChange={() => {
  1261. if (isAtBottom) {
  1262. scrollToBottomAuto()
  1263. }
  1264. }}
  1265. mode={mode}
  1266. setMode={setMode}
  1267. modeShortcutText={modeShortcutText}
  1268. />
  1269. <div id="roo-portal" />
  1270. </div>
  1271. )
  1272. }
  1273. const ChatView = forwardRef(ChatViewComponent)
  1274. export default ChatView