ChatView.tsx 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987
  1. import React, { 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 { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
  7. import useSound from "use-sound"
  8. import { LRUCache } from "lru-cache"
  9. import { Trans, useTranslation } from "react-i18next"
  10. import { useDebounceEffect } from "@src/utils/useDebounceEffect"
  11. import { appendImages } from "@src/utils/imageUtils"
  12. import type { ClineAsk, ClineMessage, McpServerUse } from "@roo-code/types"
  13. import { ClineSayBrowserAction, ClineSayTool, ExtensionMessage } from "@roo/ExtensionMessage"
  14. import { McpServer, McpTool } from "@roo/mcp"
  15. import { findLast } from "@roo/array"
  16. import { FollowUpData, SuggestionItem } from "@roo-code/types"
  17. import { combineApiRequests } from "@roo/combineApiRequests"
  18. import { combineCommandSequences } from "@roo/combineCommandSequences"
  19. import { getApiMetrics } from "@roo/getApiMetrics"
  20. import { AudioType } from "@roo/WebviewMessage"
  21. import { getAllModes } from "@roo/modes"
  22. import { ProfileValidator } from "@roo/ProfileValidator"
  23. import { getLatestTodo } from "@roo/todo"
  24. import { vscode } from "@src/utils/vscode"
  25. import {
  26. getCommandDecision,
  27. CommandDecision,
  28. findLongestPrefixMatch,
  29. parseCommand,
  30. } from "@src/utils/command-validation"
  31. import { useAppTranslation } from "@src/i18n/TranslationContext"
  32. import { useExtensionState } from "@src/context/ExtensionStateContext"
  33. import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
  34. import RooHero from "@src/components/welcome/RooHero"
  35. import RooTips from "@src/components/welcome/RooTips"
  36. import { StandardTooltip } from "@src/components/ui"
  37. import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
  38. import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
  39. import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
  40. import TelemetryBanner from "../common/TelemetryBanner"
  41. import VersionIndicator from "../common/VersionIndicator"
  42. import HistoryPreview from "../history/HistoryPreview"
  43. import Announcement from "./Announcement"
  44. import BrowserSessionRow from "./BrowserSessionRow"
  45. import ChatRow from "./ChatRow"
  46. import { ChatTextArea } from "./ChatTextArea"
  47. import TaskHeader from "./TaskHeader"
  48. import SystemPromptWarning from "./SystemPromptWarning"
  49. import ProfileViolationWarning from "./ProfileViolationWarning"
  50. import { CheckpointWarning } from "./CheckpointWarning"
  51. import { QueuedMessages } from "./QueuedMessages"
  52. import DismissibleUpsell from "../common/DismissibleUpsell"
  53. import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
  54. import { Cloud } from "lucide-react"
  55. export interface ChatViewProps {
  56. isHidden: boolean
  57. showAnnouncement: boolean
  58. hideAnnouncement: () => void
  59. }
  60. export interface ChatViewRef {
  61. acceptInput: () => void
  62. }
  63. export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit.
  64. const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
  65. const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
  66. { isHidden, showAnnouncement, hideAnnouncement },
  67. ref,
  68. ) => {
  69. const isMountedRef = useRef(true)
  70. const [audioBaseUri] = useState(() => {
  71. const w = window as any
  72. return w.AUDIO_BASE_URI || ""
  73. })
  74. const { t } = useAppTranslation()
  75. const { t: tSettings } = useTranslation("settings")
  76. const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}, ${isMac ? "⌘" : "Ctrl"} + Shift + . ${t("chat:forPreviousMode")}`
  77. const {
  78. clineMessages: messages,
  79. currentTaskItem,
  80. currentTaskTodos,
  81. taskHistory,
  82. apiConfiguration,
  83. organizationAllowList,
  84. mcpServers,
  85. alwaysAllowBrowser,
  86. alwaysAllowReadOnly,
  87. alwaysAllowReadOnlyOutsideWorkspace,
  88. alwaysAllowWrite,
  89. alwaysAllowWriteOutsideWorkspace,
  90. alwaysAllowWriteProtected,
  91. alwaysAllowExecute,
  92. alwaysAllowMcp,
  93. allowedCommands,
  94. deniedCommands,
  95. writeDelayMs,
  96. followupAutoApproveTimeoutMs,
  97. mode,
  98. setMode,
  99. autoApprovalEnabled,
  100. alwaysAllowModeSwitch,
  101. alwaysAllowSubtasks,
  102. alwaysAllowFollowupQuestions,
  103. alwaysAllowUpdateTodoList,
  104. customModes,
  105. telemetrySetting,
  106. hasSystemPromptOverride,
  107. soundEnabled,
  108. soundVolume,
  109. cloudIsAuthenticated,
  110. messageQueue = [],
  111. } = useExtensionState()
  112. const messagesRef = useRef(messages)
  113. useEffect(() => {
  114. messagesRef.current = messages
  115. }, [messages])
  116. // Leaving this less safe version here since if the first message is not a
  117. // task, then the extension is in a bad state and needs to be debugged (see
  118. // Cline.abort).
  119. const task = useMemo(() => messages.at(0), [messages])
  120. const latestTodos = useMemo(() => {
  121. // First check if we have initial todos from the state (for new subtasks)
  122. if (currentTaskTodos && currentTaskTodos.length > 0) {
  123. // Check if there are any todo updates in messages
  124. const messageBasedTodos = getLatestTodo(messages)
  125. // If there are message-based todos, they take precedence (user has updated them)
  126. if (messageBasedTodos && messageBasedTodos.length > 0) {
  127. return messageBasedTodos
  128. }
  129. // Otherwise use the initial todos from state
  130. return currentTaskTodos
  131. }
  132. // Fall back to extracting from messages
  133. return getLatestTodo(messages)
  134. }, [messages, currentTaskTodos])
  135. const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
  136. // Has to be after api_req_finished are all reduced into api_req_started messages.
  137. const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
  138. const [inputValue, setInputValue] = useState("")
  139. const inputValueRef = useRef(inputValue)
  140. const textAreaRef = useRef<HTMLTextAreaElement>(null)
  141. const [sendingDisabled, setSendingDisabled] = useState(false)
  142. const [selectedImages, setSelectedImages] = useState<string[]>([])
  143. // 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)
  144. const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
  145. const [enableButtons, setEnableButtons] = useState<boolean>(false)
  146. const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
  147. const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
  148. const [didClickCancel, setDidClickCancel] = useState(false)
  149. const virtuosoRef = useRef<VirtuosoHandle>(null)
  150. const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
  151. const prevExpandedRowsRef = useRef<Record<number, boolean>>()
  152. const scrollContainerRef = useRef<HTMLDivElement>(null)
  153. const disableAutoScrollRef = useRef(false)
  154. const [showScrollToBottom, setShowScrollToBottom] = useState(false)
  155. const [isAtBottom, setIsAtBottom] = useState(false)
  156. const lastTtsRef = useRef<string>("")
  157. const [wasStreaming, setWasStreaming] = useState<boolean>(false)
  158. const [checkpointWarning, setCheckpointWarning] = useState<
  159. { type: "WAIT_TIMEOUT" | "INIT_TIMEOUT"; timeout: number } | undefined
  160. >(undefined)
  161. const [isCondensing, setIsCondensing] = useState<boolean>(false)
  162. const [showAnnouncementModal, setShowAnnouncementModal] = useState(false)
  163. const everVisibleMessagesTsRef = useRef<LRUCache<number, boolean>>(
  164. new LRUCache({
  165. max: 100,
  166. ttl: 1000 * 60 * 5,
  167. }),
  168. )
  169. const autoApproveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
  170. const userRespondedRef = useRef<boolean>(false)
  171. const [currentFollowUpTs, setCurrentFollowUpTs] = useState<number | null>(null)
  172. const clineAskRef = useRef(clineAsk)
  173. useEffect(() => {
  174. clineAskRef.current = clineAsk
  175. }, [clineAsk])
  176. const {
  177. isOpen: isUpsellOpen,
  178. openUpsell,
  179. closeUpsell,
  180. handleConnect,
  181. } = useCloudUpsell({
  182. autoOpenOnAuth: false,
  183. })
  184. // Keep inputValueRef in sync with inputValue state
  185. useEffect(() => {
  186. inputValueRef.current = inputValue
  187. }, [inputValue])
  188. useEffect(() => {
  189. isMountedRef.current = true
  190. return () => {
  191. isMountedRef.current = false
  192. }
  193. }, [])
  194. const isProfileDisabled = useMemo(
  195. () => !!apiConfiguration && !ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList),
  196. [apiConfiguration, organizationAllowList],
  197. )
  198. // UI layout depends on the last 2 messages
  199. // (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
  200. const lastMessage = useMemo(() => messages.at(-1), [messages])
  201. const secondLastMessage = useMemo(() => messages.at(-2), [messages])
  202. // Setup sound hooks with use-sound
  203. const volume = typeof soundVolume === "number" ? soundVolume : 0.5
  204. const soundConfig = {
  205. volume,
  206. // useSound expects 'disabled' property, not 'soundEnabled'
  207. soundEnabled,
  208. }
  209. const getAudioUrl = (path: string) => `${audioBaseUri}/${path}`
  210. // Use the getAudioUrl helper function
  211. const [playNotification] = useSound(getAudioUrl("notification.wav"), soundConfig)
  212. const [playCelebration] = useSound(getAudioUrl("celebration.wav"), soundConfig)
  213. const [playProgressLoop] = useSound(getAudioUrl("progress_loop.wav"), soundConfig)
  214. function playSound(audioType: AudioType) {
  215. // Play the appropriate sound based on type
  216. // The disabled state is handled by the useSound hook configuration
  217. switch (audioType) {
  218. case "notification":
  219. playNotification()
  220. break
  221. case "celebration":
  222. playCelebration()
  223. break
  224. case "progress_loop":
  225. playProgressLoop()
  226. break
  227. default:
  228. console.warn(`Unknown audio type: ${audioType}`)
  229. }
  230. }
  231. function playTts(text: string) {
  232. vscode.postMessage({ type: "playTts", text })
  233. }
  234. useDeepCompareEffect(() => {
  235. // if last message is an ask, show user ask UI
  236. // 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.
  237. // basically as long as a task is active, the conversation history will be persisted
  238. if (lastMessage) {
  239. switch (lastMessage.type) {
  240. case "ask":
  241. // Reset user response flag when a new ask arrives to allow auto-approval
  242. userRespondedRef.current = false
  243. const isPartial = lastMessage.partial === true
  244. switch (lastMessage.ask) {
  245. case "api_req_failed":
  246. playSound("progress_loop")
  247. setSendingDisabled(true)
  248. setClineAsk("api_req_failed")
  249. setEnableButtons(true)
  250. setPrimaryButtonText(t("chat:retry.title"))
  251. setSecondaryButtonText(t("chat:startNewTask.title"))
  252. break
  253. case "mistake_limit_reached":
  254. playSound("progress_loop")
  255. setSendingDisabled(false)
  256. setClineAsk("mistake_limit_reached")
  257. setEnableButtons(true)
  258. setPrimaryButtonText(t("chat:proceedAnyways.title"))
  259. setSecondaryButtonText(t("chat:startNewTask.title"))
  260. break
  261. case "followup":
  262. if (!isPartial) {
  263. playSound("notification")
  264. }
  265. setSendingDisabled(isPartial)
  266. setClineAsk("followup")
  267. // setting enable buttons to `false` would trigger a focus grab when
  268. // the text area is enabled which is undesirable.
  269. // We have no buttons for this tool, so no problem having them "enabled"
  270. // to workaround this issue. See #1358.
  271. setEnableButtons(true)
  272. setPrimaryButtonText(undefined)
  273. setSecondaryButtonText(undefined)
  274. break
  275. case "tool":
  276. if (!isAutoApproved(lastMessage) && !isPartial) {
  277. playSound("notification")
  278. }
  279. setSendingDisabled(isPartial)
  280. setClineAsk("tool")
  281. setEnableButtons(!isPartial)
  282. const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
  283. switch (tool.tool) {
  284. case "editedExistingFile":
  285. case "appliedDiff":
  286. case "newFileCreated":
  287. case "insertContent":
  288. case "generateImage":
  289. setPrimaryButtonText(t("chat:save.title"))
  290. setSecondaryButtonText(t("chat:reject.title"))
  291. break
  292. case "finishTask":
  293. setPrimaryButtonText(t("chat:completeSubtaskAndReturn"))
  294. setSecondaryButtonText(undefined)
  295. break
  296. case "readFile":
  297. if (tool.batchFiles && Array.isArray(tool.batchFiles)) {
  298. setPrimaryButtonText(t("chat:read-batch.approve.title"))
  299. setSecondaryButtonText(t("chat:read-batch.deny.title"))
  300. } else {
  301. setPrimaryButtonText(t("chat:approve.title"))
  302. setSecondaryButtonText(t("chat:reject.title"))
  303. }
  304. break
  305. default:
  306. setPrimaryButtonText(t("chat:approve.title"))
  307. setSecondaryButtonText(t("chat:reject.title"))
  308. break
  309. }
  310. break
  311. case "browser_action_launch":
  312. if (!isAutoApproved(lastMessage) && !isPartial) {
  313. playSound("notification")
  314. }
  315. setSendingDisabled(isPartial)
  316. setClineAsk("browser_action_launch")
  317. setEnableButtons(!isPartial)
  318. setPrimaryButtonText(t("chat:approve.title"))
  319. setSecondaryButtonText(t("chat:reject.title"))
  320. break
  321. case "command":
  322. if (!isAutoApproved(lastMessage) && !isPartial) {
  323. playSound("notification")
  324. }
  325. setSendingDisabled(isPartial)
  326. setClineAsk("command")
  327. setEnableButtons(!isPartial)
  328. setPrimaryButtonText(t("chat:runCommand.title"))
  329. setSecondaryButtonText(t("chat:reject.title"))
  330. break
  331. case "command_output":
  332. setSendingDisabled(false)
  333. setClineAsk("command_output")
  334. setEnableButtons(true)
  335. setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
  336. setSecondaryButtonText(t("chat:killCommand.title"))
  337. break
  338. case "use_mcp_server":
  339. if (!isAutoApproved(lastMessage) && !isPartial) {
  340. playSound("notification")
  341. }
  342. setSendingDisabled(isPartial)
  343. setClineAsk("use_mcp_server")
  344. setEnableButtons(!isPartial)
  345. setPrimaryButtonText(t("chat:approve.title"))
  346. setSecondaryButtonText(t("chat:reject.title"))
  347. break
  348. case "completion_result":
  349. // extension waiting for feedback. but we can just present a new task button
  350. if (!isPartial) {
  351. playSound("celebration")
  352. }
  353. setSendingDisabled(isPartial)
  354. setClineAsk("completion_result")
  355. setEnableButtons(!isPartial)
  356. setPrimaryButtonText(t("chat:startNewTask.title"))
  357. setSecondaryButtonText(undefined)
  358. break
  359. case "resume_task":
  360. setSendingDisabled(false)
  361. setClineAsk("resume_task")
  362. setEnableButtons(true)
  363. setPrimaryButtonText(t("chat:resumeTask.title"))
  364. setSecondaryButtonText(t("chat:terminate.title"))
  365. setDidClickCancel(false) // special case where we reset the cancel button state
  366. break
  367. case "resume_completed_task":
  368. setSendingDisabled(false)
  369. setClineAsk("resume_completed_task")
  370. setEnableButtons(true)
  371. setPrimaryButtonText(t("chat:startNewTask.title"))
  372. setSecondaryButtonText(undefined)
  373. setDidClickCancel(false)
  374. break
  375. }
  376. break
  377. case "say":
  378. // Don't want to reset since there could be a "say" after
  379. // an "ask" while ask is waiting for response.
  380. switch (lastMessage.say) {
  381. case "api_req_retry_delayed":
  382. setSendingDisabled(true)
  383. break
  384. case "api_req_started":
  385. if (secondLastMessage?.ask === "command_output") {
  386. setSendingDisabled(true)
  387. setSelectedImages([])
  388. setClineAsk(undefined)
  389. setEnableButtons(false)
  390. }
  391. break
  392. case "api_req_finished":
  393. case "error":
  394. case "text":
  395. case "browser_action":
  396. case "browser_action_result":
  397. case "command_output":
  398. case "mcp_server_request_started":
  399. case "mcp_server_response":
  400. case "completion_result":
  401. break
  402. }
  403. break
  404. }
  405. }
  406. }, [lastMessage, secondLastMessage])
  407. useEffect(() => {
  408. if (messages.length === 0) {
  409. setSendingDisabled(false)
  410. setClineAsk(undefined)
  411. setEnableButtons(false)
  412. setPrimaryButtonText(undefined)
  413. setSecondaryButtonText(undefined)
  414. }
  415. }, [messages.length])
  416. useEffect(() => {
  417. // Reset UI states
  418. setExpandedRows({})
  419. everVisibleMessagesTsRef.current.clear() // Clear for new task
  420. setCurrentFollowUpTs(null) // Clear follow-up answered state for new task
  421. setIsCondensing(false) // Reset condensing state when switching tasks
  422. // Note: sendingDisabled is not reset here as it's managed by message effects
  423. // Clear any pending auto-approval timeout from previous task
  424. if (autoApproveTimeoutRef.current) {
  425. clearTimeout(autoApproveTimeoutRef.current)
  426. autoApproveTimeoutRef.current = null
  427. }
  428. // Reset user response flag for new task
  429. userRespondedRef.current = false
  430. }, [task?.ts])
  431. useEffect(() => {
  432. if (isHidden) {
  433. everVisibleMessagesTsRef.current.clear()
  434. }
  435. }, [isHidden])
  436. useEffect(() => {
  437. const cache = everVisibleMessagesTsRef.current
  438. return () => {
  439. cache.clear()
  440. }
  441. }, [])
  442. useEffect(() => {
  443. const prev = prevExpandedRowsRef.current
  444. let wasAnyRowExpandedByUser = false
  445. if (prev) {
  446. // Check if any row transitioned from false/undefined to true
  447. for (const [tsKey, isExpanded] of Object.entries(expandedRows)) {
  448. const ts = Number(tsKey)
  449. if (isExpanded && !(prev[ts] ?? false)) {
  450. wasAnyRowExpandedByUser = true
  451. break
  452. }
  453. }
  454. }
  455. if (wasAnyRowExpandedByUser) {
  456. disableAutoScrollRef.current = true
  457. }
  458. prevExpandedRowsRef.current = expandedRows // Store current state for next comparison
  459. }, [expandedRows])
  460. const isStreaming = useMemo(() => {
  461. // Checking clineAsk isn't enough since messages effect may be called
  462. // again for a tool for example, set clineAsk to its value, and if the
  463. // next message is not an ask then it doesn't reset. This is likely due
  464. // to how much more often we're updating messages as compared to before,
  465. // and should be resolved with optimizations as it's likely a rendering
  466. // bug. But as a final guard for now, the cancel button will show if the
  467. // last message is not an ask.
  468. const isLastAsk = !!modifiedMessages.at(-1)?.ask
  469. const isToolCurrentlyAsking =
  470. isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
  471. if (isToolCurrentlyAsking) {
  472. return false
  473. }
  474. const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
  475. if (isLastMessagePartial) {
  476. return true
  477. } else {
  478. const lastApiReqStarted = findLast(
  479. modifiedMessages,
  480. (message: ClineMessage) => message.say === "api_req_started",
  481. )
  482. if (
  483. lastApiReqStarted &&
  484. lastApiReqStarted.text !== null &&
  485. lastApiReqStarted.text !== undefined &&
  486. lastApiReqStarted.say === "api_req_started"
  487. ) {
  488. const cost = JSON.parse(lastApiReqStarted.text).cost
  489. if (cost === undefined) {
  490. return true // API request has not finished yet.
  491. }
  492. }
  493. }
  494. return false
  495. }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
  496. const markFollowUpAsAnswered = useCallback(() => {
  497. const lastFollowUpMessage = messagesRef.current.findLast((msg: ClineMessage) => msg.ask === "followup")
  498. if (lastFollowUpMessage) {
  499. setCurrentFollowUpTs(lastFollowUpMessage.ts)
  500. }
  501. }, [])
  502. const handleChatReset = useCallback(() => {
  503. // Clear any pending auto-approval timeout
  504. if (autoApproveTimeoutRef.current) {
  505. clearTimeout(autoApproveTimeoutRef.current)
  506. autoApproveTimeoutRef.current = null
  507. }
  508. // Reset user response flag for new message
  509. userRespondedRef.current = false
  510. // Only reset message-specific state, preserving mode.
  511. setInputValue("")
  512. setSendingDisabled(true)
  513. setSelectedImages([])
  514. setClineAsk(undefined)
  515. setEnableButtons(false)
  516. // Do not reset mode here as it should persist.
  517. // setPrimaryButtonText(undefined)
  518. // setSecondaryButtonText(undefined)
  519. disableAutoScrollRef.current = false
  520. }, [])
  521. /**
  522. * Handles sending messages to the extension
  523. * @param text - The message text to send
  524. * @param images - Array of image data URLs to send with the message
  525. */
  526. const handleSendMessage = useCallback(
  527. (text: string, images: string[]) => {
  528. text = text.trim()
  529. if (text || images.length > 0) {
  530. // Queue message if:
  531. // - Task is busy (sendingDisabled)
  532. // - API request in progress (isStreaming)
  533. // - Queue has items (preserve message order during drain)
  534. if (sendingDisabled || isStreaming || messageQueue.length > 0) {
  535. try {
  536. console.log("queueMessage", text, images)
  537. vscode.postMessage({ type: "queueMessage", text, images })
  538. setInputValue("")
  539. setSelectedImages([])
  540. } catch (error) {
  541. console.error(
  542. `Failed to queue message: ${error instanceof Error ? error.message : String(error)}`,
  543. )
  544. }
  545. return
  546. }
  547. // Mark that user has responded - this prevents any pending auto-approvals.
  548. userRespondedRef.current = true
  549. if (messagesRef.current.length === 0) {
  550. vscode.postMessage({ type: "newTask", text, images })
  551. } else if (clineAskRef.current) {
  552. if (clineAskRef.current === "followup") {
  553. markFollowUpAsAnswered()
  554. }
  555. // Use clineAskRef.current
  556. switch (
  557. clineAskRef.current // Use clineAskRef.current
  558. ) {
  559. case "followup":
  560. case "tool":
  561. case "browser_action_launch":
  562. case "command": // User can provide feedback to a tool or command use.
  563. case "command_output": // User can send input to command stdin.
  564. case "use_mcp_server":
  565. case "completion_result": // If this happens then the user has feedback for the completion result.
  566. case "resume_task":
  567. case "resume_completed_task":
  568. case "mistake_limit_reached":
  569. vscode.postMessage({
  570. type: "askResponse",
  571. askResponse: "messageResponse",
  572. text,
  573. images,
  574. })
  575. break
  576. // There is no other case that a textfield should be enabled.
  577. }
  578. } else {
  579. // This is a new message in an ongoing task.
  580. vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
  581. }
  582. handleChatReset()
  583. }
  584. },
  585. [handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
  586. )
  587. const handleSetChatBoxMessage = useCallback(
  588. (text: string, images: string[]) => {
  589. // Avoid nested template literals by breaking down the logic
  590. let newValue = text
  591. if (inputValue !== "") {
  592. newValue = inputValue + " " + text
  593. }
  594. setInputValue(newValue)
  595. setSelectedImages([...selectedImages, ...images])
  596. },
  597. [inputValue, selectedImages],
  598. )
  599. const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
  600. // This logic depends on the useEffect[messages] above to set clineAsk,
  601. // after which buttons are shown and we then send an askResponse to the
  602. // extension.
  603. const handlePrimaryButtonClick = useCallback(
  604. (text?: string, images?: string[]) => {
  605. // Mark that user has responded
  606. userRespondedRef.current = true
  607. const trimmedInput = text?.trim()
  608. switch (clineAsk) {
  609. case "api_req_failed":
  610. case "command":
  611. case "tool":
  612. case "browser_action_launch":
  613. case "use_mcp_server":
  614. case "resume_task":
  615. case "mistake_limit_reached":
  616. // Only send text/images if they exist
  617. if (trimmedInput || (images && images.length > 0)) {
  618. vscode.postMessage({
  619. type: "askResponse",
  620. askResponse: "yesButtonClicked",
  621. text: trimmedInput,
  622. images: images,
  623. })
  624. // Clear input state after sending
  625. setInputValue("")
  626. setSelectedImages([])
  627. } else {
  628. vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
  629. }
  630. break
  631. case "completion_result":
  632. case "resume_completed_task":
  633. // Waiting for feedback, but we can just present a new task button
  634. startNewTask()
  635. break
  636. case "command_output":
  637. vscode.postMessage({ type: "terminalOperation", terminalOperation: "continue" })
  638. break
  639. }
  640. setSendingDisabled(true)
  641. setClineAsk(undefined)
  642. setEnableButtons(false)
  643. },
  644. [clineAsk, startNewTask],
  645. )
  646. const handleSecondaryButtonClick = useCallback(
  647. (text?: string, images?: string[]) => {
  648. // Mark that user has responded
  649. userRespondedRef.current = true
  650. const trimmedInput = text?.trim()
  651. if (isStreaming) {
  652. vscode.postMessage({ type: "cancelTask" })
  653. setDidClickCancel(true)
  654. return
  655. }
  656. switch (clineAsk) {
  657. case "api_req_failed":
  658. case "mistake_limit_reached":
  659. case "resume_task":
  660. startNewTask()
  661. break
  662. case "command":
  663. case "tool":
  664. case "browser_action_launch":
  665. case "use_mcp_server":
  666. // Only send text/images if they exist
  667. if (trimmedInput || (images && images.length > 0)) {
  668. vscode.postMessage({
  669. type: "askResponse",
  670. askResponse: "noButtonClicked",
  671. text: trimmedInput,
  672. images: images,
  673. })
  674. // Clear input state after sending
  675. setInputValue("")
  676. setSelectedImages([])
  677. } else {
  678. // Responds to the API with a "This operation failed" and lets it try again
  679. vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
  680. }
  681. break
  682. case "command_output":
  683. vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
  684. break
  685. }
  686. setSendingDisabled(true)
  687. setClineAsk(undefined)
  688. setEnableButtons(false)
  689. },
  690. [clineAsk, startNewTask, isStreaming],
  691. )
  692. const { info: model } = useSelectedModel(apiConfiguration)
  693. const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
  694. const shouldDisableImages = !model?.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
  695. const handleMessage = useCallback(
  696. (e: MessageEvent) => {
  697. const message: ExtensionMessage = e.data
  698. switch (message.type) {
  699. case "action":
  700. switch (message.action!) {
  701. case "didBecomeVisible":
  702. if (!isHidden && !sendingDisabled && !enableButtons) {
  703. textAreaRef.current?.focus()
  704. }
  705. break
  706. case "focusInput":
  707. textAreaRef.current?.focus()
  708. break
  709. }
  710. break
  711. case "selectedImages":
  712. // Only handle selectedImages if it's not for editing context
  713. // When context is "edit", ChatRow will handle the images
  714. if (message.context !== "edit") {
  715. setSelectedImages((prevImages: string[]) =>
  716. appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE),
  717. )
  718. }
  719. break
  720. case "invoke":
  721. switch (message.invoke!) {
  722. case "newChat":
  723. handleChatReset()
  724. break
  725. case "sendMessage":
  726. handleSendMessage(message.text ?? "", message.images ?? [])
  727. break
  728. case "setChatBoxMessage":
  729. handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
  730. break
  731. case "primaryButtonClick":
  732. handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
  733. break
  734. case "secondaryButtonClick":
  735. handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
  736. break
  737. }
  738. break
  739. case "condenseTaskContextResponse":
  740. if (message.text && message.text === currentTaskItem?.id) {
  741. if (isCondensing && sendingDisabled) {
  742. setSendingDisabled(false)
  743. }
  744. setIsCondensing(false)
  745. }
  746. break
  747. case "checkpointInitWarning":
  748. setCheckpointWarning(message.checkpointWarning)
  749. break
  750. }
  751. // textAreaRef.current is not explicitly required here since React
  752. // guarantees that ref will be stable across re-renders, and we're
  753. // not using its value but its reference.
  754. },
  755. [
  756. isCondensing,
  757. isHidden,
  758. sendingDisabled,
  759. enableButtons,
  760. currentTaskItem,
  761. handleChatReset,
  762. handleSendMessage,
  763. handleSetChatBoxMessage,
  764. handlePrimaryButtonClick,
  765. handleSecondaryButtonClick,
  766. setCheckpointWarning,
  767. ],
  768. )
  769. useEvent("message", handleMessage)
  770. // NOTE: the VSCode window needs to be focused for this to work.
  771. useMount(() => textAreaRef.current?.focus())
  772. const visibleMessages = useMemo(() => {
  773. // Pre-compute checkpoint hashes that have associated user messages for O(1) lookup
  774. const userMessageCheckpointHashes = new Set<string>()
  775. modifiedMessages.forEach((msg) => {
  776. if (
  777. msg.say === "user_feedback" &&
  778. msg.checkpoint &&
  779. (msg.checkpoint as any).type === "user_message" &&
  780. (msg.checkpoint as any).hash
  781. ) {
  782. userMessageCheckpointHashes.add((msg.checkpoint as any).hash)
  783. }
  784. })
  785. // Remove the 500-message limit to prevent array index shifting
  786. // Virtuoso is designed to efficiently handle large lists through virtualization
  787. const newVisibleMessages = modifiedMessages.filter((message) => {
  788. // Filter out checkpoint_saved messages that should be suppressed
  789. if (message.say === "checkpoint_saved") {
  790. // Check if this checkpoint has the suppressMessage flag set
  791. if (
  792. message.checkpoint &&
  793. typeof message.checkpoint === "object" &&
  794. "suppressMessage" in message.checkpoint &&
  795. message.checkpoint.suppressMessage
  796. ) {
  797. return false
  798. }
  799. // Also filter out checkpoint messages associated with user messages (legacy behavior)
  800. if (message.text && userMessageCheckpointHashes.has(message.text)) {
  801. return false
  802. }
  803. }
  804. if (everVisibleMessagesTsRef.current.has(message.ts)) {
  805. const alwaysHiddenOnceProcessedAsk: ClineAsk[] = [
  806. "api_req_failed",
  807. "resume_task",
  808. "resume_completed_task",
  809. ]
  810. const alwaysHiddenOnceProcessedSay = [
  811. "api_req_finished",
  812. "api_req_retried",
  813. "api_req_deleted",
  814. "mcp_server_request_started",
  815. ]
  816. if (message.ask && alwaysHiddenOnceProcessedAsk.includes(message.ask)) return false
  817. if (message.say && alwaysHiddenOnceProcessedSay.includes(message.say)) return false
  818. if (message.say === "text" && (message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
  819. return false
  820. }
  821. return true
  822. }
  823. switch (message.ask) {
  824. case "completion_result":
  825. if (message.text === "") return false
  826. break
  827. case "api_req_failed":
  828. case "resume_task":
  829. case "resume_completed_task":
  830. return false
  831. }
  832. switch (message.say) {
  833. case "api_req_finished":
  834. case "api_req_retried":
  835. case "api_req_deleted":
  836. return false
  837. case "api_req_retry_delayed":
  838. const last1 = modifiedMessages.at(-1)
  839. const last2 = modifiedMessages.at(-2)
  840. if (last1?.ask === "resume_task" && last2 === message) {
  841. return true
  842. } else if (message !== last1) {
  843. return false
  844. }
  845. break
  846. case "text":
  847. if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) return false
  848. break
  849. case "mcp_server_request_started":
  850. return false
  851. }
  852. return true
  853. })
  854. const viewportStart = Math.max(0, newVisibleMessages.length - 100)
  855. newVisibleMessages
  856. .slice(viewportStart)
  857. .forEach((msg: ClineMessage) => everVisibleMessagesTsRef.current.set(msg.ts, true))
  858. return newVisibleMessages
  859. }, [modifiedMessages])
  860. useEffect(() => {
  861. const cleanupInterval = setInterval(() => {
  862. const cache = everVisibleMessagesTsRef.current
  863. const currentMessageIds = new Set(modifiedMessages.map((m: ClineMessage) => m.ts))
  864. const viewportMessages = visibleMessages.slice(Math.max(0, visibleMessages.length - 100))
  865. const viewportMessageIds = new Set(viewportMessages.map((m: ClineMessage) => m.ts))
  866. cache.forEach((_value: boolean, key: number) => {
  867. if (!currentMessageIds.has(key) && !viewportMessageIds.has(key)) {
  868. cache.delete(key)
  869. }
  870. })
  871. }, 60000)
  872. return () => clearInterval(cleanupInterval)
  873. }, [modifiedMessages, visibleMessages])
  874. useDebounceEffect(
  875. () => {
  876. if (!isHidden && !sendingDisabled && !enableButtons) {
  877. textAreaRef.current?.focus()
  878. }
  879. },
  880. 50,
  881. [isHidden, sendingDisabled, enableButtons],
  882. )
  883. const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => {
  884. if (message?.type === "ask") {
  885. if (!message.text) {
  886. return true
  887. }
  888. const tool = JSON.parse(message.text)
  889. return [
  890. "readFile",
  891. "listFiles",
  892. "listFilesTopLevel",
  893. "listFilesRecursive",
  894. "listCodeDefinitionNames",
  895. "searchFiles",
  896. "codebaseSearch",
  897. "runSlashCommand",
  898. ].includes(tool.tool)
  899. }
  900. return false
  901. }, [])
  902. const isWriteToolAction = useCallback((message: ClineMessage | undefined) => {
  903. if (message?.type === "ask") {
  904. if (!message.text) {
  905. return true
  906. }
  907. const tool = JSON.parse(message.text)
  908. return ["editedExistingFile", "appliedDiff", "newFileCreated", "insertContent", "generateImage"].includes(
  909. tool.tool,
  910. )
  911. }
  912. return false
  913. }, [])
  914. const isMcpToolAlwaysAllowed = useCallback(
  915. (message: ClineMessage | undefined) => {
  916. if (message?.type === "ask" && message.ask === "use_mcp_server") {
  917. if (!message.text) {
  918. return true
  919. }
  920. const mcpServerUse = JSON.parse(message.text) as McpServerUse
  921. if (mcpServerUse.type === "use_mcp_tool" && mcpServerUse.toolName) {
  922. const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
  923. const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
  924. return tool?.alwaysAllow || false
  925. }
  926. }
  927. return false
  928. },
  929. [mcpServers],
  930. )
  931. // Get the command decision using unified validation logic
  932. const getCommandDecisionForMessage = useCallback(
  933. (message: ClineMessage | undefined): CommandDecision => {
  934. if (message?.type !== "ask") return "ask_user"
  935. return getCommandDecision(message.text || "", allowedCommands || [], deniedCommands || [])
  936. },
  937. [allowedCommands, deniedCommands],
  938. )
  939. // Check if a command message should be auto-approved.
  940. const isAllowedCommand = useCallback(
  941. (message: ClineMessage | undefined): boolean => {
  942. return getCommandDecisionForMessage(message) === "auto_approve"
  943. },
  944. [getCommandDecisionForMessage],
  945. )
  946. // Check if a command message should be auto-denied.
  947. const isDeniedCommand = useCallback(
  948. (message: ClineMessage | undefined): boolean => {
  949. return getCommandDecisionForMessage(message) === "auto_deny"
  950. },
  951. [getCommandDecisionForMessage],
  952. )
  953. // Helper function to get the denied prefix for a command
  954. const getDeniedPrefix = useCallback(
  955. (command: string): string | null => {
  956. if (!command || !deniedCommands?.length) return null
  957. // Parse the command into sub-commands and check each one
  958. const subCommands = parseCommand(command)
  959. for (const cmd of subCommands) {
  960. const deniedMatch = findLongestPrefixMatch(cmd, deniedCommands)
  961. if (deniedMatch) {
  962. return deniedMatch
  963. }
  964. }
  965. return null
  966. },
  967. [deniedCommands],
  968. )
  969. // Create toggles object for useAutoApprovalState hook
  970. const autoApprovalToggles = useAutoApprovalToggles()
  971. const { hasEnabledOptions } = useAutoApprovalState(autoApprovalToggles, autoApprovalEnabled)
  972. const isAutoApproved = useCallback(
  973. (message: ClineMessage | undefined) => {
  974. // First check if auto-approval is enabled AND we have at least one permission
  975. if (!autoApprovalEnabled || !message || message.type !== "ask") {
  976. return false
  977. }
  978. // Use the hook's result instead of duplicating the logic
  979. if (!hasEnabledOptions) {
  980. return false
  981. }
  982. if (message.ask === "followup") {
  983. return alwaysAllowFollowupQuestions
  984. }
  985. if (message.ask === "browser_action_launch") {
  986. return alwaysAllowBrowser
  987. }
  988. if (message.ask === "use_mcp_server") {
  989. // Check if it's a tool or resource access
  990. if (!message.text) {
  991. return false
  992. }
  993. try {
  994. const mcpServerUse = JSON.parse(message.text) as McpServerUse
  995. if (mcpServerUse.type === "use_mcp_tool") {
  996. // For tools, check if the specific tool is always allowed
  997. return alwaysAllowMcp && isMcpToolAlwaysAllowed(message)
  998. } else if (mcpServerUse.type === "access_mcp_resource") {
  999. // For resources, auto-approve if MCP is always allowed
  1000. // Resources don't have individual alwaysAllow settings like tools do
  1001. return alwaysAllowMcp
  1002. }
  1003. } catch (error) {
  1004. console.error("Failed to parse MCP server use message:", error)
  1005. return false
  1006. }
  1007. return false
  1008. }
  1009. if (message.ask === "command") {
  1010. return alwaysAllowExecute && isAllowedCommand(message)
  1011. }
  1012. // For read/write operations, check if it's outside workspace and if
  1013. // we have permission for that.
  1014. if (message.ask === "tool") {
  1015. let tool: any = {}
  1016. try {
  1017. tool = JSON.parse(message.text || "{}")
  1018. } catch (error) {
  1019. console.error("Failed to parse tool:", error)
  1020. }
  1021. if (!tool) {
  1022. return false
  1023. }
  1024. if (tool?.tool === "updateTodoList") {
  1025. return alwaysAllowUpdateTodoList
  1026. }
  1027. if (tool?.tool === "fetchInstructions") {
  1028. if (tool.content === "create_mode") {
  1029. return alwaysAllowModeSwitch
  1030. }
  1031. if (tool.content === "create_mcp_server") {
  1032. return alwaysAllowMcp
  1033. }
  1034. }
  1035. if (tool?.tool === "switchMode") {
  1036. return alwaysAllowModeSwitch
  1037. }
  1038. if (["newTask", "finishTask"].includes(tool?.tool)) {
  1039. return alwaysAllowSubtasks
  1040. }
  1041. const isOutsideWorkspace = !!tool.isOutsideWorkspace
  1042. const isProtected = message.isProtected
  1043. if (isReadOnlyToolAction(message)) {
  1044. return alwaysAllowReadOnly && (!isOutsideWorkspace || alwaysAllowReadOnlyOutsideWorkspace)
  1045. }
  1046. if (isWriteToolAction(message)) {
  1047. return (
  1048. alwaysAllowWrite &&
  1049. (!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace) &&
  1050. (!isProtected || alwaysAllowWriteProtected)
  1051. )
  1052. }
  1053. }
  1054. return false
  1055. },
  1056. [
  1057. autoApprovalEnabled,
  1058. hasEnabledOptions,
  1059. alwaysAllowBrowser,
  1060. alwaysAllowReadOnly,
  1061. alwaysAllowReadOnlyOutsideWorkspace,
  1062. isReadOnlyToolAction,
  1063. alwaysAllowWrite,
  1064. alwaysAllowWriteOutsideWorkspace,
  1065. alwaysAllowWriteProtected,
  1066. isWriteToolAction,
  1067. alwaysAllowExecute,
  1068. isAllowedCommand,
  1069. alwaysAllowMcp,
  1070. isMcpToolAlwaysAllowed,
  1071. alwaysAllowModeSwitch,
  1072. alwaysAllowFollowupQuestions,
  1073. alwaysAllowSubtasks,
  1074. alwaysAllowUpdateTodoList,
  1075. ],
  1076. )
  1077. useEffect(() => {
  1078. // This ensures the first message is not read, future user messages are
  1079. // labeled as `user_feedback`.
  1080. if (lastMessage && messages.length > 1) {
  1081. if (
  1082. lastMessage.text && // has text
  1083. (lastMessage.say === "text" || lastMessage.say === "completion_result") && // is a text message
  1084. !lastMessage.partial && // not a partial message
  1085. !lastMessage.text.startsWith("{") // not a json object
  1086. ) {
  1087. let text = lastMessage?.text || ""
  1088. const mermaidRegex = /```mermaid[\s\S]*?```/g
  1089. // remove mermaid diagrams from text
  1090. text = text.replace(mermaidRegex, "")
  1091. // remove markdown from text
  1092. text = removeMd(text)
  1093. // ensure message is not a duplicate of last read message
  1094. if (text !== lastTtsRef.current) {
  1095. try {
  1096. playTts(text)
  1097. lastTtsRef.current = text
  1098. } catch (error) {
  1099. console.error("Failed to execute text-to-speech:", error)
  1100. }
  1101. }
  1102. }
  1103. }
  1104. // Update previous value.
  1105. setWasStreaming(isStreaming)
  1106. }, [isStreaming, lastMessage, wasStreaming, isAutoApproved, messages.length])
  1107. const isBrowserSessionMessage = (message: ClineMessage): boolean => {
  1108. // Which of visible messages are browser session messages, see above.
  1109. if (message.type === "ask") {
  1110. return ["browser_action_launch"].includes(message.ask!)
  1111. }
  1112. if (message.type === "say") {
  1113. return ["api_req_started", "text", "browser_action", "browser_action_result"].includes(message.say!)
  1114. }
  1115. return false
  1116. }
  1117. const groupedMessages = useMemo(() => {
  1118. const result: (ClineMessage | ClineMessage[])[] = []
  1119. let currentGroup: ClineMessage[] = []
  1120. let isInBrowserSession = false
  1121. const endBrowserSession = () => {
  1122. if (currentGroup.length > 0) {
  1123. result.push([...currentGroup])
  1124. currentGroup = []
  1125. isInBrowserSession = false
  1126. }
  1127. }
  1128. visibleMessages.forEach((message: ClineMessage) => {
  1129. if (message.ask === "browser_action_launch") {
  1130. // Complete existing browser session if any.
  1131. endBrowserSession()
  1132. // Start new.
  1133. isInBrowserSession = true
  1134. currentGroup.push(message)
  1135. } else if (isInBrowserSession) {
  1136. // End session if `api_req_started` is cancelled.
  1137. if (message.say === "api_req_started") {
  1138. // Get last `api_req_started` in currentGroup to check if
  1139. // it's cancelled. If it is then this api req is not part
  1140. // of the current browser session.
  1141. const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
  1142. if (lastApiReqStarted?.text !== null && lastApiReqStarted?.text !== undefined) {
  1143. const info = JSON.parse(lastApiReqStarted.text)
  1144. const isCancelled = info.cancelReason !== null && info.cancelReason !== undefined
  1145. if (isCancelled) {
  1146. endBrowserSession()
  1147. result.push(message)
  1148. return
  1149. }
  1150. }
  1151. }
  1152. if (isBrowserSessionMessage(message)) {
  1153. currentGroup.push(message)
  1154. // Check if this is a close action
  1155. if (message.say === "browser_action") {
  1156. const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
  1157. if (browserAction.action === "close") {
  1158. endBrowserSession()
  1159. }
  1160. }
  1161. } else {
  1162. // complete existing browser session if any
  1163. endBrowserSession()
  1164. result.push(message)
  1165. }
  1166. } else {
  1167. result.push(message)
  1168. }
  1169. })
  1170. // Handle case where browser session is the last group
  1171. if (currentGroup.length > 0) {
  1172. result.push([...currentGroup])
  1173. }
  1174. if (isCondensing) {
  1175. // Show indicator after clicking condense button
  1176. result.push({
  1177. type: "say",
  1178. say: "condense_context",
  1179. ts: Date.now(),
  1180. partial: true,
  1181. })
  1182. }
  1183. return result
  1184. }, [isCondensing, visibleMessages])
  1185. // scrolling
  1186. const scrollToBottomSmooth = useMemo(
  1187. () =>
  1188. debounce(() => virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" }), 10, {
  1189. immediate: true,
  1190. }),
  1191. [],
  1192. )
  1193. useEffect(() => {
  1194. return () => {
  1195. if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") {
  1196. ;(scrollToBottomSmooth as any).cancel()
  1197. }
  1198. }
  1199. }, [scrollToBottomSmooth])
  1200. const scrollToBottomAuto = useCallback(() => {
  1201. virtuosoRef.current?.scrollTo({
  1202. top: Number.MAX_SAFE_INTEGER,
  1203. behavior: "auto", // Instant causes crash.
  1204. })
  1205. }, [])
  1206. const handleSetExpandedRow = useCallback(
  1207. (ts: number, expand?: boolean) => {
  1208. setExpandedRows((prev: Record<number, boolean>) => ({
  1209. ...prev,
  1210. [ts]: expand === undefined ? !prev[ts] : expand,
  1211. }))
  1212. },
  1213. [setExpandedRows], // setExpandedRows is stable
  1214. )
  1215. // Scroll when user toggles certain rows.
  1216. const toggleRowExpansion = useCallback(
  1217. (ts: number) => {
  1218. handleSetExpandedRow(ts)
  1219. // The logic to set disableAutoScrollRef.current = true on expansion
  1220. // is now handled by the useEffect hook that observes expandedRows.
  1221. },
  1222. [handleSetExpandedRow],
  1223. )
  1224. const handleRowHeightChange = useCallback(
  1225. (isTaller: boolean) => {
  1226. if (!disableAutoScrollRef.current) {
  1227. if (isTaller) {
  1228. scrollToBottomSmooth()
  1229. } else {
  1230. setTimeout(() => scrollToBottomAuto(), 0)
  1231. }
  1232. }
  1233. },
  1234. [scrollToBottomSmooth, scrollToBottomAuto],
  1235. )
  1236. useEffect(() => {
  1237. let timer: ReturnType<typeof setTimeout> | undefined
  1238. if (!disableAutoScrollRef.current) {
  1239. timer = setTimeout(() => scrollToBottomSmooth(), 50)
  1240. }
  1241. return () => {
  1242. if (timer) {
  1243. clearTimeout(timer)
  1244. }
  1245. }
  1246. }, [groupedMessages.length, scrollToBottomSmooth])
  1247. const handleWheel = useCallback((event: Event) => {
  1248. const wheelEvent = event as WheelEvent
  1249. if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
  1250. if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
  1251. // User scrolled up
  1252. disableAutoScrollRef.current = true
  1253. }
  1254. }
  1255. }, [])
  1256. useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
  1257. // Effect to clear checkpoint warning when messages appear or task changes
  1258. useEffect(() => {
  1259. if (isHidden || !task) {
  1260. setCheckpointWarning(undefined)
  1261. }
  1262. }, [modifiedMessages.length, isStreaming, isHidden, task])
  1263. const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask")
  1264. const switchToMode = useCallback(
  1265. (modeSlug: string): void => {
  1266. // Update local state and notify extension to sync mode change.
  1267. setMode(modeSlug)
  1268. // Send the mode switch message.
  1269. vscode.postMessage({ type: "mode", text: modeSlug })
  1270. },
  1271. [setMode],
  1272. )
  1273. const handleSuggestionClickInRow = useCallback(
  1274. (suggestion: SuggestionItem, event?: React.MouseEvent) => {
  1275. // Mark that user has responded if this is a manual click (not auto-approval)
  1276. if (event) {
  1277. userRespondedRef.current = true
  1278. }
  1279. // Mark the current follow-up question as answered when a suggestion is clicked
  1280. if (clineAsk === "followup" && !event?.shiftKey) {
  1281. markFollowUpAsAnswered()
  1282. }
  1283. // Check if we need to switch modes
  1284. if (suggestion.mode) {
  1285. // Only switch modes if it's a manual click (event exists) or auto-approval is allowed
  1286. const isManualClick = !!event
  1287. if (isManualClick || alwaysAllowModeSwitch) {
  1288. // Switch mode without waiting
  1289. switchToMode(suggestion.mode)
  1290. }
  1291. }
  1292. if (event?.shiftKey) {
  1293. // Always append to existing text, don't overwrite
  1294. setInputValue((currentValue: string) => {
  1295. return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer
  1296. })
  1297. } else {
  1298. // Don't clear the input value when sending a follow-up choice
  1299. // The message should be sent but the text area should preserve what the user typed
  1300. const preservedInput = inputValueRef.current
  1301. handleSendMessage(suggestion.answer, [])
  1302. // Restore the input value after sending
  1303. setInputValue(preservedInput)
  1304. }
  1305. },
  1306. [handleSendMessage, setInputValue, switchToMode, alwaysAllowModeSwitch, clineAsk, markFollowUpAsAnswered],
  1307. )
  1308. const handleBatchFileResponse = useCallback((response: { [key: string]: boolean }) => {
  1309. // Handle batch file response, e.g., for file uploads
  1310. vscode.postMessage({ type: "askResponse", askResponse: "objectResponse", text: JSON.stringify(response) })
  1311. }, [])
  1312. // Handler for when FollowUpSuggest component unmounts
  1313. const handleFollowUpUnmount = useCallback(() => {
  1314. // Mark that user has responded
  1315. userRespondedRef.current = true
  1316. }, [])
  1317. const itemContent = useCallback(
  1318. (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => {
  1319. // browser session group
  1320. if (Array.isArray(messageOrGroup)) {
  1321. return (
  1322. <BrowserSessionRow
  1323. messages={messageOrGroup}
  1324. isLast={index === groupedMessages.length - 1}
  1325. lastModifiedMessage={modifiedMessages.at(-1)}
  1326. onHeightChange={handleRowHeightChange}
  1327. isStreaming={isStreaming}
  1328. isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
  1329. onToggleExpand={(messageTs: number) => {
  1330. setExpandedRows((prev: Record<number, boolean>) => ({
  1331. ...prev,
  1332. [messageTs]: !prev[messageTs],
  1333. }))
  1334. }}
  1335. />
  1336. )
  1337. }
  1338. const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved")
  1339. // regular message
  1340. return (
  1341. <ChatRow
  1342. key={messageOrGroup.ts}
  1343. message={messageOrGroup}
  1344. isExpanded={expandedRows[messageOrGroup.ts] || false}
  1345. onToggleExpand={toggleRowExpansion} // This was already stabilized
  1346. lastModifiedMessage={modifiedMessages.at(-1)} // Original direct access
  1347. isLast={index === groupedMessages.length - 1} // Original direct access
  1348. onHeightChange={handleRowHeightChange}
  1349. isStreaming={isStreaming}
  1350. onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
  1351. onBatchFileResponse={handleBatchFileResponse}
  1352. onFollowUpUnmount={handleFollowUpUnmount}
  1353. isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs}
  1354. editable={
  1355. messageOrGroup.type === "ask" &&
  1356. messageOrGroup.ask === "tool" &&
  1357. (() => {
  1358. let tool: any = {}
  1359. try {
  1360. tool = JSON.parse(messageOrGroup.text || "{}")
  1361. } catch (_) {
  1362. if (messageOrGroup.text?.includes("updateTodoList")) {
  1363. tool = { tool: "updateTodoList" }
  1364. }
  1365. }
  1366. if (tool.tool === "updateTodoList" && alwaysAllowUpdateTodoList) {
  1367. return false
  1368. }
  1369. return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText
  1370. })()
  1371. }
  1372. hasCheckpoint={hasCheckpoint}
  1373. />
  1374. )
  1375. },
  1376. [
  1377. expandedRows,
  1378. toggleRowExpansion,
  1379. modifiedMessages,
  1380. groupedMessages.length,
  1381. handleRowHeightChange,
  1382. isStreaming,
  1383. handleSuggestionClickInRow,
  1384. handleBatchFileResponse,
  1385. handleFollowUpUnmount,
  1386. currentFollowUpTs,
  1387. alwaysAllowUpdateTodoList,
  1388. enableButtons,
  1389. primaryButtonText,
  1390. ],
  1391. )
  1392. useEffect(() => {
  1393. if (autoApproveTimeoutRef.current) {
  1394. clearTimeout(autoApproveTimeoutRef.current)
  1395. autoApproveTimeoutRef.current = null
  1396. }
  1397. if (!clineAsk || !enableButtons) {
  1398. return
  1399. }
  1400. // Exit early if user has already responded
  1401. if (userRespondedRef.current) {
  1402. return
  1403. }
  1404. const autoApproveOrReject = async () => {
  1405. // Check for auto-reject first (commands that should be denied)
  1406. if (lastMessage?.ask === "command" && isDeniedCommand(lastMessage)) {
  1407. // Get the denied prefix for the localized message
  1408. const deniedPrefix = getDeniedPrefix(lastMessage.text || "")
  1409. if (deniedPrefix) {
  1410. // Create the localized auto-deny message and send it with the rejection
  1411. const autoDenyMessage = tSettings("autoApprove.execute.autoDenied", { prefix: deniedPrefix })
  1412. vscode.postMessage({
  1413. type: "askResponse",
  1414. askResponse: "noButtonClicked",
  1415. text: autoDenyMessage,
  1416. })
  1417. } else {
  1418. // Auto-reject denied commands immediately if no prefix found
  1419. vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
  1420. }
  1421. setSendingDisabled(true)
  1422. setClineAsk(undefined)
  1423. setEnableButtons(false)
  1424. return
  1425. }
  1426. // Then check for auto-approve
  1427. if (lastMessage?.ask && isAutoApproved(lastMessage)) {
  1428. // Special handling for follow-up questions
  1429. if (lastMessage.ask === "followup") {
  1430. // Handle invalid JSON
  1431. let followUpData: FollowUpData = {}
  1432. try {
  1433. followUpData = JSON.parse(lastMessage.text || "{}") as FollowUpData
  1434. } catch (error) {
  1435. console.error("Failed to parse follow-up data:", error)
  1436. return
  1437. }
  1438. if (followUpData && followUpData.suggest && followUpData.suggest.length > 0) {
  1439. // Wait for the configured timeout before auto-selecting the first suggestion
  1440. await new Promise<void>((resolve) => {
  1441. autoApproveTimeoutRef.current = setTimeout(() => {
  1442. autoApproveTimeoutRef.current = null
  1443. resolve()
  1444. }, followupAutoApproveTimeoutMs)
  1445. })
  1446. // Check if user responded manually
  1447. if (userRespondedRef.current) {
  1448. return
  1449. }
  1450. // Get the first suggestion
  1451. const firstSuggestion = followUpData.suggest[0]
  1452. // Handle the suggestion click
  1453. handleSuggestionClickInRow(firstSuggestion)
  1454. return
  1455. }
  1456. } else if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
  1457. await new Promise<void>((resolve) => {
  1458. autoApproveTimeoutRef.current = setTimeout(() => {
  1459. autoApproveTimeoutRef.current = null
  1460. resolve()
  1461. }, writeDelayMs)
  1462. })
  1463. }
  1464. vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
  1465. setSendingDisabled(true)
  1466. setClineAsk(undefined)
  1467. setEnableButtons(false)
  1468. }
  1469. }
  1470. autoApproveOrReject()
  1471. return () => {
  1472. if (autoApproveTimeoutRef.current) {
  1473. clearTimeout(autoApproveTimeoutRef.current)
  1474. autoApproveTimeoutRef.current = null
  1475. }
  1476. }
  1477. }, [
  1478. clineAsk,
  1479. enableButtons,
  1480. handlePrimaryButtonClick,
  1481. alwaysAllowBrowser,
  1482. alwaysAllowReadOnly,
  1483. alwaysAllowReadOnlyOutsideWorkspace,
  1484. alwaysAllowWrite,
  1485. alwaysAllowWriteOutsideWorkspace,
  1486. alwaysAllowExecute,
  1487. followupAutoApproveTimeoutMs,
  1488. alwaysAllowMcp,
  1489. messages,
  1490. allowedCommands,
  1491. deniedCommands,
  1492. mcpServers,
  1493. isAutoApproved,
  1494. lastMessage,
  1495. writeDelayMs,
  1496. isWriteToolAction,
  1497. alwaysAllowFollowupQuestions,
  1498. handleSuggestionClickInRow,
  1499. isAllowedCommand,
  1500. isDeniedCommand,
  1501. getDeniedPrefix,
  1502. tSettings,
  1503. ])
  1504. // Function to handle mode switching
  1505. const switchToNextMode = useCallback(() => {
  1506. const allModes = getAllModes(customModes)
  1507. const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
  1508. const nextModeIndex = (currentModeIndex + 1) % allModes.length
  1509. // Update local state and notify extension to sync mode change
  1510. switchToMode(allModes[nextModeIndex].slug)
  1511. }, [mode, customModes, switchToMode])
  1512. // Function to handle switching to previous mode
  1513. const switchToPreviousMode = useCallback(() => {
  1514. const allModes = getAllModes(customModes)
  1515. const currentModeIndex = allModes.findIndex((m) => m.slug === mode)
  1516. const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length
  1517. // Update local state and notify extension to sync mode change
  1518. switchToMode(allModes[previousModeIndex].slug)
  1519. }, [mode, customModes, switchToMode])
  1520. // Add keyboard event handler
  1521. const handleKeyDown = useCallback(
  1522. (event: KeyboardEvent) => {
  1523. // Check for Command/Ctrl + Period (with or without Shift)
  1524. // Using event.key to respect keyboard layouts (e.g., Dvorak)
  1525. if ((event.metaKey || event.ctrlKey) && event.key === ".") {
  1526. event.preventDefault() // Prevent default browser behavior
  1527. if (event.shiftKey) {
  1528. // Shift + Period = Previous mode
  1529. switchToPreviousMode()
  1530. } else {
  1531. // Just Period = Next mode
  1532. switchToNextMode()
  1533. }
  1534. }
  1535. },
  1536. [switchToNextMode, switchToPreviousMode],
  1537. )
  1538. useEffect(() => {
  1539. window.addEventListener("keydown", handleKeyDown)
  1540. return () => {
  1541. window.removeEventListener("keydown", handleKeyDown)
  1542. }
  1543. }, [handleKeyDown])
  1544. useImperativeHandle(ref, () => ({
  1545. acceptInput: () => {
  1546. if (enableButtons && primaryButtonText) {
  1547. handlePrimaryButtonClick(inputValue, selectedImages)
  1548. } else if (!sendingDisabled && !isProfileDisabled && (inputValue.trim() || selectedImages.length > 0)) {
  1549. handleSendMessage(inputValue, selectedImages)
  1550. }
  1551. },
  1552. }))
  1553. const handleCondenseContext = (taskId: string) => {
  1554. if (isCondensing || sendingDisabled) {
  1555. return
  1556. }
  1557. setIsCondensing(true)
  1558. setSendingDisabled(true)
  1559. vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
  1560. }
  1561. const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
  1562. return (
  1563. <div
  1564. data-testid="chat-view"
  1565. className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
  1566. {telemetrySetting === "unset" && <TelemetryBanner />}
  1567. {(showAnnouncement || showAnnouncementModal) && (
  1568. <Announcement
  1569. hideAnnouncement={() => {
  1570. if (showAnnouncementModal) {
  1571. setShowAnnouncementModal(false)
  1572. }
  1573. if (showAnnouncement) {
  1574. hideAnnouncement()
  1575. }
  1576. }}
  1577. />
  1578. )}
  1579. {task ? (
  1580. <>
  1581. <TaskHeader
  1582. task={task}
  1583. tokensIn={apiMetrics.totalTokensIn}
  1584. tokensOut={apiMetrics.totalTokensOut}
  1585. cacheWrites={apiMetrics.totalCacheWrites}
  1586. cacheReads={apiMetrics.totalCacheReads}
  1587. totalCost={apiMetrics.totalCost}
  1588. contextTokens={apiMetrics.contextTokens}
  1589. buttonsDisabled={sendingDisabled}
  1590. handleCondenseContext={handleCondenseContext}
  1591. todos={latestTodos}
  1592. />
  1593. {hasSystemPromptOverride && (
  1594. <div className="px-3">
  1595. <SystemPromptWarning />
  1596. </div>
  1597. )}
  1598. {checkpointWarning && (
  1599. <div className="px-3">
  1600. <CheckpointWarning warning={checkpointWarning} />
  1601. </div>
  1602. )}
  1603. </>
  1604. ) : (
  1605. <div className="flex flex-col h-full justify-center p-6 min-h-0 overflow-y-auto gap-4 relative">
  1606. <div className="flex flex-col items-start gap-2 justify-center h-full min-[400px]:px-6">
  1607. <VersionIndicator
  1608. onClick={() => setShowAnnouncementModal(true)}
  1609. className="absolute top-2 right-3 z-10"
  1610. />
  1611. <div className="flex flex-col gap-4 w-full">
  1612. <RooHero />
  1613. {/* Show RooTips when authenticated or when user is new */}
  1614. {taskHistory.length < 6 && <RooTips />}
  1615. {/* Everyone should see their task history if any */}
  1616. {taskHistory.length > 0 && <HistoryPreview />}
  1617. </div>
  1618. {/* Logged out users should see a one-time upsell, but not for brand new users */}
  1619. {!cloudIsAuthenticated && taskHistory.length >= 6 && (
  1620. <DismissibleUpsell
  1621. upsellId="taskList2"
  1622. icon={<Cloud className="size-5 mt-0.5 shrink-0" />}
  1623. onClick={() => openUpsell()}
  1624. dismissOnClick={false}
  1625. className="!bg-vscode-editor-background mt-6 border-border rounded-xl pl-4 pr-3 py-3 !text-base">
  1626. <Trans
  1627. i18nKey="cloud:upsell.taskList"
  1628. components={{
  1629. learnMoreLink: <VSCodeLink href="#" />,
  1630. }}
  1631. />
  1632. </DismissibleUpsell>
  1633. )}
  1634. </div>
  1635. </div>
  1636. )}
  1637. {task && (
  1638. <>
  1639. <div className="grow flex" ref={scrollContainerRef}>
  1640. <Virtuoso
  1641. ref={virtuosoRef}
  1642. key={task.ts}
  1643. className="scrollable grow overflow-y-scroll mb-1"
  1644. increaseViewportBy={{ top: 3_000, bottom: 1000 }}
  1645. data={groupedMessages}
  1646. itemContent={itemContent}
  1647. atBottomStateChange={(isAtBottom: boolean) => {
  1648. setIsAtBottom(isAtBottom)
  1649. if (isAtBottom) {
  1650. disableAutoScrollRef.current = false
  1651. }
  1652. setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
  1653. }}
  1654. atBottomThreshold={10}
  1655. initialTopMostItemIndex={groupedMessages.length - 1}
  1656. />
  1657. </div>
  1658. {areButtonsVisible && (
  1659. <div
  1660. className={`flex h-9 items-center mb-1 px-[15px] ${
  1661. showScrollToBottom
  1662. ? "opacity-100"
  1663. : enableButtons || (isStreaming && !didClickCancel)
  1664. ? "opacity-100"
  1665. : "opacity-50"
  1666. }`}>
  1667. {showScrollToBottom ? (
  1668. <StandardTooltip content={t("chat:scrollToBottom")}>
  1669. <VSCodeButton
  1670. appearance="secondary"
  1671. className="flex-[2]"
  1672. onClick={() => {
  1673. scrollToBottomSmooth()
  1674. disableAutoScrollRef.current = false
  1675. }}>
  1676. <span className="codicon codicon-chevron-down"></span>
  1677. </VSCodeButton>
  1678. </StandardTooltip>
  1679. ) : (
  1680. <>
  1681. {primaryButtonText && !isStreaming && (
  1682. <StandardTooltip
  1683. content={
  1684. primaryButtonText === t("chat:retry.title")
  1685. ? t("chat:retry.tooltip")
  1686. : primaryButtonText === t("chat:save.title")
  1687. ? t("chat:save.tooltip")
  1688. : primaryButtonText === t("chat:approve.title")
  1689. ? t("chat:approve.tooltip")
  1690. : primaryButtonText === t("chat:runCommand.title")
  1691. ? t("chat:runCommand.tooltip")
  1692. : primaryButtonText === t("chat:startNewTask.title")
  1693. ? t("chat:startNewTask.tooltip")
  1694. : primaryButtonText === t("chat:resumeTask.title")
  1695. ? t("chat:resumeTask.tooltip")
  1696. : primaryButtonText ===
  1697. t("chat:proceedAnyways.title")
  1698. ? t("chat:proceedAnyways.tooltip")
  1699. : primaryButtonText ===
  1700. t("chat:proceedWhileRunning.title")
  1701. ? t("chat:proceedWhileRunning.tooltip")
  1702. : undefined
  1703. }>
  1704. <VSCodeButton
  1705. appearance="primary"
  1706. disabled={!enableButtons}
  1707. className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
  1708. onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
  1709. {primaryButtonText}
  1710. </VSCodeButton>
  1711. </StandardTooltip>
  1712. )}
  1713. {(secondaryButtonText || isStreaming) && (
  1714. <StandardTooltip
  1715. content={
  1716. isStreaming
  1717. ? t("chat:cancel.tooltip")
  1718. : secondaryButtonText === t("chat:startNewTask.title")
  1719. ? t("chat:startNewTask.tooltip")
  1720. : secondaryButtonText === t("chat:reject.title")
  1721. ? t("chat:reject.tooltip")
  1722. : secondaryButtonText === t("chat:terminate.title")
  1723. ? t("chat:terminate.tooltip")
  1724. : undefined
  1725. }>
  1726. <VSCodeButton
  1727. appearance="secondary"
  1728. disabled={!enableButtons && !(isStreaming && !didClickCancel)}
  1729. className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
  1730. onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
  1731. {isStreaming ? t("chat:cancel.title") : secondaryButtonText}
  1732. </VSCodeButton>
  1733. </StandardTooltip>
  1734. )}
  1735. </>
  1736. )}
  1737. </div>
  1738. )}
  1739. </>
  1740. )}
  1741. <QueuedMessages
  1742. queue={messageQueue}
  1743. onRemove={(index) => {
  1744. if (messageQueue[index]) {
  1745. vscode.postMessage({ type: "removeQueuedMessage", text: messageQueue[index].id })
  1746. }
  1747. }}
  1748. onUpdate={(index, newText) => {
  1749. if (messageQueue[index]) {
  1750. vscode.postMessage({
  1751. type: "editQueuedMessage",
  1752. payload: { id: messageQueue[index].id, text: newText, images: messageQueue[index].images },
  1753. })
  1754. }
  1755. }}
  1756. />
  1757. <ChatTextArea
  1758. ref={textAreaRef}
  1759. inputValue={inputValue}
  1760. setInputValue={setInputValue}
  1761. sendingDisabled={sendingDisabled || isProfileDisabled}
  1762. selectApiConfigDisabled={sendingDisabled && clineAsk !== "api_req_failed"}
  1763. placeholderText={placeholderText}
  1764. selectedImages={selectedImages}
  1765. setSelectedImages={setSelectedImages}
  1766. onSend={() => handleSendMessage(inputValue, selectedImages)}
  1767. onSelectImages={selectImages}
  1768. shouldDisableImages={shouldDisableImages}
  1769. onHeightChange={() => {
  1770. if (isAtBottom) {
  1771. scrollToBottomAuto()
  1772. }
  1773. }}
  1774. mode={mode}
  1775. setMode={setMode}
  1776. modeShortcutText={modeShortcutText}
  1777. />
  1778. {isProfileDisabled && (
  1779. <div className="px-3">
  1780. <ProfileViolationWarning />
  1781. </div>
  1782. )}
  1783. <div id="roo-portal" />
  1784. <CloudUpsellDialog open={isUpsellOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
  1785. </div>
  1786. )
  1787. }
  1788. const ChatView = forwardRef(ChatViewComponent)
  1789. export default ChatView