ChatRow.tsx 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
  2. import deepEqual from "fast-deep-equal"
  3. import React, { memo, useEffect, useMemo, useRef, useState } from "react"
  4. import { useSize } from "react-use"
  5. import { useCopyToClipboard } from "../../utils/clipboard"
  6. import {
  7. ClineApiReqInfo,
  8. ClineAskUseMcpServer,
  9. ClineMessage,
  10. ClineSayTool,
  11. } from "../../../../src/shared/ExtensionMessage"
  12. import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
  13. import { useExtensionState } from "../../context/ExtensionStateContext"
  14. import { findMatchingResourceOrTemplate } from "../../utils/mcp"
  15. import { vscode } from "../../utils/vscode"
  16. import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
  17. import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
  18. import MarkdownBlock from "../common/MarkdownBlock"
  19. import ReasoningBlock from "./ReasoningBlock"
  20. import Thumbnails from "../common/Thumbnails"
  21. import McpResourceRow from "../mcp/McpResourceRow"
  22. import McpToolRow from "../mcp/McpToolRow"
  23. import { highlightMentions } from "./TaskHeader"
  24. import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
  25. interface ChatRowProps {
  26. message: ClineMessage
  27. isExpanded: boolean
  28. onToggleExpand: () => void
  29. lastModifiedMessage?: ClineMessage
  30. isLast: boolean
  31. onHeightChange: (isTaller: boolean) => void
  32. isStreaming: boolean
  33. }
  34. interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
  35. const ChatRow = memo(
  36. (props: ChatRowProps) => {
  37. const { isLast, onHeightChange, message } = props
  38. // Store the previous height to compare with the current height
  39. // This allows us to detect changes without causing re-renders
  40. const prevHeightRef = useRef(0)
  41. const [chatrow, { height }] = useSize(
  42. <div
  43. style={{
  44. padding: "10px 6px 10px 15px",
  45. }}>
  46. <ChatRowContent {...props} />
  47. </div>,
  48. )
  49. useEffect(() => {
  50. // used for partials, command output, etc.
  51. // NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
  52. const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
  53. // height starts off at Infinity
  54. if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
  55. if (!isInitialRender) {
  56. onHeightChange(height > prevHeightRef.current)
  57. }
  58. prevHeightRef.current = height
  59. }
  60. }, [height, isLast, onHeightChange, message])
  61. // we cannot return null as virtuoso does not support it, so we use a separate visibleMessages array to filter out messages that should not be rendered
  62. return chatrow
  63. },
  64. // memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
  65. deepEqual,
  66. )
  67. export default ChatRow
  68. export const ChatRowContent = ({
  69. message,
  70. isExpanded,
  71. onToggleExpand,
  72. lastModifiedMessage,
  73. isLast,
  74. isStreaming,
  75. }: ChatRowContentProps) => {
  76. const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
  77. const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
  78. // Auto-collapse reasoning when new messages arrive
  79. useEffect(() => {
  80. if (!isLast && message.say === "reasoning") {
  81. setReasoningCollapsed(true)
  82. }
  83. }, [isLast, message.say])
  84. const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
  85. if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
  86. const info: ClineApiReqInfo = JSON.parse(message.text)
  87. return [info.cost, info.cancelReason, info.streamingFailedMessage]
  88. }
  89. return [undefined, undefined, undefined]
  90. }, [message.text, message.say])
  91. // when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
  92. const apiRequestFailedMessage =
  93. isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
  94. ? lastModifiedMessage?.text
  95. : undefined
  96. const isCommandExecuting =
  97. isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
  98. const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
  99. const type = message.type === "ask" ? message.ask : message.say
  100. const normalColor = "var(--vscode-foreground)"
  101. const errorColor = "var(--vscode-errorForeground)"
  102. const successColor = "var(--vscode-charts-green)"
  103. const cancelledColor = "var(--vscode-descriptionForeground)"
  104. const [icon, title] = useMemo(() => {
  105. switch (type) {
  106. case "error":
  107. return [
  108. <span
  109. className="codicon codicon-error"
  110. style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
  111. <span style={{ color: errorColor, fontWeight: "bold" }}>Error</span>,
  112. ]
  113. case "mistake_limit_reached":
  114. return [
  115. <span
  116. className="codicon codicon-error"
  117. style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
  118. <span style={{ color: errorColor, fontWeight: "bold" }}>Roo is having trouble...</span>,
  119. ]
  120. case "command":
  121. return [
  122. isCommandExecuting ? (
  123. <ProgressIndicator />
  124. ) : (
  125. <span
  126. className="codicon codicon-terminal"
  127. style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
  128. ),
  129. <span style={{ color: normalColor, fontWeight: "bold" }}>Roo wants to execute this command:</span>,
  130. ]
  131. case "use_mcp_server":
  132. const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
  133. return [
  134. isMcpServerResponding ? (
  135. <ProgressIndicator />
  136. ) : (
  137. <span
  138. className="codicon codicon-server"
  139. style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
  140. ),
  141. <span style={{ color: normalColor, fontWeight: "bold" }}>
  142. Roo wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
  143. <code>{mcpServerUse.serverName}</code> MCP server:
  144. </span>,
  145. ]
  146. case "completion_result":
  147. return [
  148. <span
  149. className="codicon codicon-check"
  150. style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
  151. <span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
  152. ]
  153. case "api_req_retry_delayed":
  154. return []
  155. case "api_req_started":
  156. const getIconSpan = (iconName: string, color: string) => (
  157. <div
  158. style={{
  159. width: 16,
  160. height: 16,
  161. display: "flex",
  162. alignItems: "center",
  163. justifyContent: "center",
  164. }}>
  165. <span
  166. className={`codicon codicon-${iconName}`}
  167. style={{
  168. color,
  169. fontSize: 16,
  170. marginBottom: "-1.5px",
  171. }}></span>
  172. </div>
  173. )
  174. return [
  175. apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
  176. apiReqCancelReason === "user_cancelled" ? (
  177. getIconSpan("error", cancelledColor)
  178. ) : (
  179. getIconSpan("error", errorColor)
  180. )
  181. ) : cost !== null && cost !== undefined ? (
  182. getIconSpan("check", successColor)
  183. ) : apiRequestFailedMessage ? (
  184. getIconSpan("error", errorColor)
  185. ) : (
  186. <ProgressIndicator />
  187. ),
  188. apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
  189. apiReqCancelReason === "user_cancelled" ? (
  190. <span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
  191. ) : (
  192. <span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
  193. )
  194. ) : cost !== null && cost !== undefined ? (
  195. <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
  196. ) : apiRequestFailedMessage ? (
  197. <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
  198. ) : (
  199. <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
  200. ),
  201. ]
  202. case "followup":
  203. return [
  204. <span
  205. className="codicon codicon-question"
  206. style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
  207. <span style={{ color: normalColor, fontWeight: "bold" }}>Roo has a question:</span>,
  208. ]
  209. default:
  210. return [null, null]
  211. }
  212. }, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage])
  213. const headerStyle: React.CSSProperties = {
  214. display: "flex",
  215. alignItems: "center",
  216. gap: "10px",
  217. marginBottom: "10px",
  218. }
  219. const pStyle: React.CSSProperties = {
  220. margin: 0,
  221. whiteSpace: "pre-wrap",
  222. wordBreak: "break-word",
  223. overflowWrap: "anywhere",
  224. }
  225. const tool = useMemo(() => {
  226. if (message.ask === "tool" || message.say === "tool") {
  227. return JSON.parse(message.text || "{}") as ClineSayTool
  228. }
  229. return null
  230. }, [message.ask, message.say, message.text])
  231. if (tool) {
  232. const toolIcon = (name: string) => (
  233. <span
  234. className={`codicon codicon-${name}`}
  235. style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
  236. )
  237. switch (tool.tool) {
  238. case "editedExistingFile":
  239. case "appliedDiff":
  240. return (
  241. <>
  242. <div style={headerStyle}>
  243. {toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
  244. <span style={{ fontWeight: "bold" }}>Roo wants to edit this file:</span>
  245. </div>
  246. <CodeAccordian
  247. isLoading={message.partial}
  248. diff={tool.diff!}
  249. path={tool.path!}
  250. isExpanded={isExpanded}
  251. onToggleExpand={onToggleExpand}
  252. />
  253. </>
  254. )
  255. case "newFileCreated":
  256. return (
  257. <>
  258. <div style={headerStyle}>
  259. {toolIcon("new-file")}
  260. <span style={{ fontWeight: "bold" }}>Roo wants to create a new file:</span>
  261. </div>
  262. <CodeAccordian
  263. isLoading={message.partial}
  264. code={tool.content!}
  265. path={tool.path!}
  266. isExpanded={isExpanded}
  267. onToggleExpand={onToggleExpand}
  268. />
  269. </>
  270. )
  271. case "readFile":
  272. return (
  273. <>
  274. <div style={headerStyle}>
  275. {toolIcon("file-code")}
  276. <span style={{ fontWeight: "bold" }}>
  277. {message.type === "ask" ? "Roo wants to read this file:" : "Roo read this file:"}
  278. </span>
  279. </div>
  280. {/* <CodeAccordian
  281. code={tool.content!}
  282. path={tool.path!}
  283. isExpanded={isExpanded}
  284. onToggleExpand={onToggleExpand}
  285. /> */}
  286. <div
  287. style={{
  288. borderRadius: 3,
  289. backgroundColor: CODE_BLOCK_BG_COLOR,
  290. overflow: "hidden",
  291. border: "1px solid var(--vscode-editorGroup-border)",
  292. }}>
  293. <div
  294. style={{
  295. color: "var(--vscode-descriptionForeground)",
  296. display: "flex",
  297. alignItems: "center",
  298. padding: "9px 10px",
  299. cursor: "pointer",
  300. userSelect: "none",
  301. WebkitUserSelect: "none",
  302. MozUserSelect: "none",
  303. msUserSelect: "none",
  304. }}
  305. onClick={() => {
  306. vscode.postMessage({ type: "openFile", text: tool.content })
  307. }}>
  308. {tool.path?.startsWith(".") && <span>.</span>}
  309. <span
  310. style={{
  311. whiteSpace: "nowrap",
  312. overflow: "hidden",
  313. textOverflow: "ellipsis",
  314. marginRight: "8px",
  315. direction: "rtl",
  316. textAlign: "left",
  317. }}>
  318. {removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
  319. </span>
  320. <div style={{ flexGrow: 1 }}></div>
  321. <span
  322. className={`codicon codicon-link-external`}
  323. style={{ fontSize: 13.5, margin: "1px 0" }}></span>
  324. </div>
  325. </div>
  326. </>
  327. )
  328. case "listFilesTopLevel":
  329. return (
  330. <>
  331. <div style={headerStyle}>
  332. {toolIcon("folder-opened")}
  333. <span style={{ fontWeight: "bold" }}>
  334. {message.type === "ask"
  335. ? "Roo wants to view the top level files in this directory:"
  336. : "Roo viewed the top level files in this directory:"}
  337. </span>
  338. </div>
  339. <CodeAccordian
  340. code={tool.content!}
  341. path={tool.path!}
  342. language="shell-session"
  343. isExpanded={isExpanded}
  344. onToggleExpand={onToggleExpand}
  345. />
  346. </>
  347. )
  348. case "listFilesRecursive":
  349. return (
  350. <>
  351. <div style={headerStyle}>
  352. {toolIcon("folder-opened")}
  353. <span style={{ fontWeight: "bold" }}>
  354. {message.type === "ask"
  355. ? "Roo wants to recursively view all files in this directory:"
  356. : "Roo recursively viewed all files in this directory:"}
  357. </span>
  358. </div>
  359. <CodeAccordian
  360. code={tool.content!}
  361. path={tool.path!}
  362. language="shell-session"
  363. isExpanded={isExpanded}
  364. onToggleExpand={onToggleExpand}
  365. />
  366. </>
  367. )
  368. case "listCodeDefinitionNames":
  369. return (
  370. <>
  371. <div style={headerStyle}>
  372. {toolIcon("file-code")}
  373. <span style={{ fontWeight: "bold" }}>
  374. {message.type === "ask"
  375. ? "Roo wants to view source code definition names used in this directory:"
  376. : "Roo viewed source code definition names used in this directory:"}
  377. </span>
  378. </div>
  379. <CodeAccordian
  380. code={tool.content!}
  381. path={tool.path!}
  382. isExpanded={isExpanded}
  383. onToggleExpand={onToggleExpand}
  384. />
  385. </>
  386. )
  387. case "searchFiles":
  388. return (
  389. <>
  390. <div style={headerStyle}>
  391. {toolIcon("search")}
  392. <span style={{ fontWeight: "bold" }}>
  393. {message.type === "ask" ? (
  394. <>
  395. Roo wants to search this directory for <code>{tool.regex}</code>:
  396. </>
  397. ) : (
  398. <>
  399. Roo searched this directory for <code>{tool.regex}</code>:
  400. </>
  401. )}
  402. </span>
  403. </div>
  404. <CodeAccordian
  405. code={tool.content!}
  406. path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
  407. language="plaintext"
  408. isExpanded={isExpanded}
  409. onToggleExpand={onToggleExpand}
  410. />
  411. </>
  412. )
  413. // case "inspectSite":
  414. // const isInspecting =
  415. // isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images
  416. // return (
  417. // <>
  418. // <div style={headerStyle}>
  419. // {isInspecting ? <ProgressIndicator /> : toolIcon("inspect")}
  420. // <span style={{ fontWeight: "bold" }}>
  421. // {message.type === "ask" ? (
  422. // <>Roo wants to inspect this website:</>
  423. // ) : (
  424. // <>Roo is inspecting this website:</>
  425. // )}
  426. // </span>
  427. // </div>
  428. // <div
  429. // style={{
  430. // borderRadius: 3,
  431. // border: "1px solid var(--vscode-editorGroup-border)",
  432. // overflow: "hidden",
  433. // backgroundColor: CODE_BLOCK_BG_COLOR,
  434. // }}>
  435. // <CodeBlock source={`${"```"}shell\n${tool.path}\n${"```"}`} forceWrap={true} />
  436. // </div>
  437. // </>
  438. // )
  439. case "switchMode":
  440. return (
  441. <>
  442. <div style={headerStyle}>
  443. {toolIcon("symbol-enum")}
  444. <span style={{ fontWeight: "bold" }}>
  445. {message.type === "ask" ? (
  446. <>
  447. Roo wants to switch to <code>{tool.mode}</code> mode
  448. {tool.reason ? ` because: ${tool.reason}` : ""}
  449. </>
  450. ) : (
  451. <>
  452. Roo switched to <code>{tool.mode}</code> mode
  453. {tool.reason ? ` because: ${tool.reason}` : ""}
  454. </>
  455. )}
  456. </span>
  457. </div>
  458. </>
  459. )
  460. case "newTask":
  461. return (
  462. <>
  463. <div style={headerStyle}>
  464. {toolIcon("new-file")}
  465. <span style={{ fontWeight: "bold" }}>
  466. Roo wants to create a new task in <code>{tool.mode}</code> mode:
  467. </span>
  468. </div>
  469. <div style={{ paddingLeft: "26px", marginTop: "4px" }}>
  470. <code>{tool.content}</code>
  471. </div>
  472. </>
  473. )
  474. default:
  475. return null
  476. }
  477. }
  478. switch (message.type) {
  479. case "say":
  480. switch (message.say) {
  481. case "reasoning":
  482. return (
  483. <ReasoningBlock
  484. content={message.text || ""}
  485. isCollapsed={reasoningCollapsed}
  486. onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
  487. />
  488. )
  489. case "api_req_started":
  490. return (
  491. <>
  492. <div
  493. style={{
  494. ...headerStyle,
  495. marginBottom:
  496. ((cost === null || cost === undefined) && apiRequestFailedMessage) ||
  497. apiReqStreamingFailedMessage
  498. ? 10
  499. : 0,
  500. justifyContent: "space-between",
  501. cursor: "pointer",
  502. userSelect: "none",
  503. WebkitUserSelect: "none",
  504. MozUserSelect: "none",
  505. msUserSelect: "none",
  506. }}
  507. onClick={onToggleExpand}>
  508. <div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
  509. {icon}
  510. {title}
  511. <VSCodeBadge
  512. style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
  513. ${Number(cost || 0)?.toFixed(4)}
  514. </VSCodeBadge>
  515. </div>
  516. <span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
  517. </div>
  518. {(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
  519. apiReqStreamingFailedMessage) && (
  520. <>
  521. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
  522. {apiRequestFailedMessage || apiReqStreamingFailedMessage}
  523. {apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
  524. <>
  525. <br />
  526. <br />
  527. It seems like you're having Windows PowerShell issues, please see this{" "}
  528. <a
  529. href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
  530. style={{ color: "inherit", textDecoration: "underline" }}>
  531. troubleshooting guide
  532. </a>
  533. .
  534. </>
  535. )}
  536. </p>
  537. {/* {apiProvider === "" && (
  538. <div
  539. style={{
  540. display: "flex",
  541. alignItems: "center",
  542. backgroundColor:
  543. "color-mix(in srgb, var(--vscode-errorForeground) 20%, transparent)",
  544. color: "var(--vscode-editor-foreground)",
  545. padding: "6px 8px",
  546. borderRadius: "3px",
  547. margin: "10px 0 0 0",
  548. fontSize: "12px",
  549. }}>
  550. <i
  551. className="codicon codicon-warning"
  552. style={{
  553. marginRight: 6,
  554. fontSize: 16,
  555. color: "var(--vscode-errorForeground)",
  556. }}></i>
  557. <span>
  558. Uh-oh, this could be a problem on end. We've been alerted and
  559. will resolve this ASAP. You can also{" "}
  560. <a
  561. href=""
  562. style={{ color: "inherit", textDecoration: "underline" }}>
  563. contact us
  564. </a>
  565. .
  566. </span>
  567. </div>
  568. )} */}
  569. </>
  570. )}
  571. {isExpanded && (
  572. <div style={{ marginTop: "10px" }}>
  573. <CodeAccordian
  574. code={JSON.parse(message.text || "{}").request}
  575. language="markdown"
  576. isExpanded={true}
  577. onToggleExpand={onToggleExpand}
  578. />
  579. </div>
  580. )}
  581. </>
  582. )
  583. case "api_req_finished":
  584. return null // we should never see this message type
  585. case "text":
  586. return (
  587. <div>
  588. <Markdown markdown={message.text} partial={message.partial} />
  589. </div>
  590. )
  591. case "user_feedback":
  592. return (
  593. <div
  594. style={{
  595. backgroundColor: "var(--vscode-badge-background)",
  596. color: "var(--vscode-badge-foreground)",
  597. borderRadius: "3px",
  598. padding: "9px",
  599. whiteSpace: "pre-line",
  600. wordWrap: "break-word",
  601. }}>
  602. <div
  603. style={{
  604. display: "flex",
  605. justifyContent: "space-between",
  606. alignItems: "flex-start",
  607. gap: "10px",
  608. }}>
  609. <span style={{ display: "block", flexGrow: 1, padding: "4px" }}>
  610. {highlightMentions(message.text)}
  611. </span>
  612. <VSCodeButton
  613. appearance="icon"
  614. style={{
  615. padding: "3px",
  616. flexShrink: 0,
  617. height: "24px",
  618. marginTop: "-3px",
  619. marginBottom: "-3px",
  620. marginRight: "-6px",
  621. }}
  622. disabled={isStreaming}
  623. onClick={(e) => {
  624. e.stopPropagation()
  625. vscode.postMessage({
  626. type: "deleteMessage",
  627. value: message.ts,
  628. })
  629. }}>
  630. <span className="codicon codicon-trash"></span>
  631. </VSCodeButton>
  632. </div>
  633. {message.images && message.images.length > 0 && (
  634. <Thumbnails images={message.images} style={{ marginTop: "8px" }} />
  635. )}
  636. </div>
  637. )
  638. case "user_feedback_diff":
  639. const tool = JSON.parse(message.text || "{}") as ClineSayTool
  640. return (
  641. <div
  642. style={{
  643. marginTop: -10,
  644. width: "100%",
  645. }}>
  646. <CodeAccordian
  647. diff={tool.diff!}
  648. isFeedback={true}
  649. isExpanded={isExpanded}
  650. onToggleExpand={onToggleExpand}
  651. />
  652. </div>
  653. )
  654. case "error":
  655. return (
  656. <>
  657. {title && (
  658. <div style={headerStyle}>
  659. {icon}
  660. {title}
  661. </div>
  662. )}
  663. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
  664. </>
  665. )
  666. case "completion_result":
  667. return (
  668. <>
  669. <div style={headerStyle}>
  670. {icon}
  671. {title}
  672. </div>
  673. <div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
  674. <Markdown markdown={message.text} />
  675. </div>
  676. </>
  677. )
  678. case "shell_integration_warning":
  679. return (
  680. <>
  681. <div
  682. style={{
  683. display: "flex",
  684. flexDirection: "column",
  685. backgroundColor: "rgba(255, 191, 0, 0.1)",
  686. padding: 8,
  687. borderRadius: 3,
  688. fontSize: 12,
  689. }}>
  690. <div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
  691. <i
  692. className="codicon codicon-warning"
  693. style={{
  694. marginRight: 8,
  695. fontSize: 18,
  696. color: "#FFA500",
  697. }}></i>
  698. <span style={{ fontWeight: 500, color: "#FFA500" }}>
  699. Shell Integration Unavailable
  700. </span>
  701. </div>
  702. <div>
  703. Roo won't be able to view the command's output. Please update VSCode (
  704. <code>CMD/CTRL + Shift + P</code> → "Update") and make sure you're using a supported
  705. shell: zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> →
  706. "Terminal: Select Default Profile").{" "}
  707. <a
  708. href="https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Shell-Integration-Unavailable"
  709. style={{ color: "inherit", textDecoration: "underline" }}>
  710. Still having trouble?
  711. </a>
  712. </div>
  713. </div>
  714. </>
  715. )
  716. case "mcp_server_response":
  717. return (
  718. <>
  719. <div style={{ paddingTop: 0 }}>
  720. <div
  721. style={{
  722. marginBottom: "4px",
  723. opacity: 0.8,
  724. fontSize: "12px",
  725. textTransform: "uppercase",
  726. }}>
  727. Response
  728. </div>
  729. <CodeAccordian
  730. code={message.text}
  731. language="json"
  732. isExpanded={true}
  733. onToggleExpand={onToggleExpand}
  734. />
  735. </div>
  736. </>
  737. )
  738. case "checkpoint_saved":
  739. return (
  740. <CheckpointSaved
  741. ts={message.ts!}
  742. commitHash={message.text!}
  743. checkpoint={message.checkpoint}
  744. currentCheckpointHash={currentCheckpoint}
  745. />
  746. )
  747. default:
  748. return (
  749. <>
  750. {title && (
  751. <div style={headerStyle}>
  752. {icon}
  753. {title}
  754. </div>
  755. )}
  756. <div style={{ paddingTop: 10 }}>
  757. <Markdown markdown={message.text} partial={message.partial} />
  758. </div>
  759. </>
  760. )
  761. }
  762. case "ask":
  763. switch (message.ask) {
  764. case "mistake_limit_reached":
  765. return (
  766. <>
  767. <div style={headerStyle}>
  768. {icon}
  769. {title}
  770. </div>
  771. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
  772. </>
  773. )
  774. case "command":
  775. const splitMessage = (text: string) => {
  776. const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
  777. if (outputIndex === -1) {
  778. return { command: text, output: "" }
  779. }
  780. return {
  781. command: text.slice(0, outputIndex).trim(),
  782. output: text
  783. .slice(outputIndex + COMMAND_OUTPUT_STRING.length)
  784. .trim()
  785. .split("")
  786. .map((char) => {
  787. switch (char) {
  788. case "\t":
  789. return "→ "
  790. case "\b":
  791. return "⌫"
  792. case "\f":
  793. return "⏏"
  794. case "\v":
  795. return "⇳"
  796. default:
  797. return char
  798. }
  799. })
  800. .join(""),
  801. }
  802. }
  803. const { command, output } = splitMessage(message.text || "")
  804. return (
  805. <>
  806. <div style={headerStyle}>
  807. {icon}
  808. {title}
  809. </div>
  810. {/* <Terminal
  811. rawOutput={command + (output ? "\n" + output : "")}
  812. shouldAllowInput={!!isCommandExecuting && output.length > 0}
  813. /> */}
  814. <div
  815. style={{
  816. borderRadius: 3,
  817. border: "1px solid var(--vscode-editorGroup-border)",
  818. overflow: "hidden",
  819. backgroundColor: CODE_BLOCK_BG_COLOR,
  820. }}>
  821. <CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
  822. {output.length > 0 && (
  823. <div style={{ width: "100%" }}>
  824. <div
  825. onClick={onToggleExpand}
  826. style={{
  827. display: "flex",
  828. alignItems: "center",
  829. gap: "4px",
  830. width: "100%",
  831. justifyContent: "flex-start",
  832. cursor: "pointer",
  833. padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
  834. }}>
  835. <span
  836. className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
  837. <span style={{ fontSize: "0.8em" }}>Command Output</span>
  838. </div>
  839. {isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
  840. </div>
  841. )}
  842. </div>
  843. </>
  844. )
  845. case "use_mcp_server":
  846. const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
  847. const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
  848. return (
  849. <>
  850. <div style={headerStyle}>
  851. {icon}
  852. {title}
  853. </div>
  854. <div
  855. style={{
  856. background: "var(--vscode-textCodeBlock-background)",
  857. borderRadius: "3px",
  858. padding: "8px 10px",
  859. marginTop: "8px",
  860. }}>
  861. {useMcpServer.type === "access_mcp_resource" && (
  862. <McpResourceRow
  863. item={{
  864. // Use the matched resource/template details, with fallbacks
  865. ...(findMatchingResourceOrTemplate(
  866. useMcpServer.uri || "",
  867. server?.resources,
  868. server?.resourceTemplates,
  869. ) || {
  870. name: "",
  871. mimeType: "",
  872. description: "",
  873. }),
  874. // Always use the actual URI from the request
  875. uri: useMcpServer.uri || "",
  876. }}
  877. />
  878. )}
  879. {useMcpServer.type === "use_mcp_tool" && (
  880. <>
  881. <div onClick={(e) => e.stopPropagation()}>
  882. <McpToolRow
  883. tool={{
  884. name: useMcpServer.toolName || "",
  885. description:
  886. server?.tools?.find(
  887. (tool) => tool.name === useMcpServer.toolName,
  888. )?.description || "",
  889. alwaysAllow:
  890. server?.tools?.find(
  891. (tool) => tool.name === useMcpServer.toolName,
  892. )?.alwaysAllow || false,
  893. }}
  894. serverName={useMcpServer.serverName}
  895. alwaysAllowMcp={alwaysAllowMcp}
  896. />
  897. </div>
  898. {useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
  899. <div style={{ marginTop: "8px" }}>
  900. <div
  901. style={{
  902. marginBottom: "4px",
  903. opacity: 0.8,
  904. fontSize: "12px",
  905. textTransform: "uppercase",
  906. }}>
  907. Arguments
  908. </div>
  909. <CodeAccordian
  910. code={useMcpServer.arguments}
  911. language="json"
  912. isExpanded={true}
  913. onToggleExpand={onToggleExpand}
  914. />
  915. </div>
  916. )}
  917. </>
  918. )}
  919. </div>
  920. </>
  921. )
  922. case "completion_result":
  923. if (message.text) {
  924. return (
  925. <div>
  926. <div style={headerStyle}>
  927. {icon}
  928. {title}
  929. </div>
  930. <div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
  931. <Markdown markdown={message.text} partial={message.partial} />
  932. </div>
  933. </div>
  934. )
  935. } else {
  936. return null // Don't render anything when we get a completion_result ask without text
  937. }
  938. case "followup":
  939. return (
  940. <>
  941. {title && (
  942. <div style={headerStyle}>
  943. {icon}
  944. {title}
  945. </div>
  946. )}
  947. <div style={{ paddingTop: 10 }}>
  948. <Markdown markdown={message.text} />
  949. </div>
  950. </>
  951. )
  952. default:
  953. return null
  954. }
  955. }
  956. }
  957. export const ProgressIndicator = () => (
  958. <div
  959. style={{
  960. width: "16px",
  961. height: "16px",
  962. display: "flex",
  963. alignItems: "center",
  964. justifyContent: "center",
  965. }}>
  966. <div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
  967. <VSCodeProgressRing />
  968. </div>
  969. </div>
  970. )
  971. const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
  972. const [isHovering, setIsHovering] = useState(false)
  973. const { copyWithFeedback } = useCopyToClipboard(200) // shorter feedback duration for copy button flash
  974. return (
  975. <div
  976. onMouseEnter={() => setIsHovering(true)}
  977. onMouseLeave={() => setIsHovering(false)}
  978. style={{ position: "relative" }}>
  979. <div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
  980. <MarkdownBlock markdown={markdown} />
  981. </div>
  982. {markdown && !partial && isHovering && (
  983. <div
  984. style={{
  985. position: "absolute",
  986. bottom: "-4px",
  987. right: "8px",
  988. opacity: 0,
  989. animation: "fadeIn 0.2s ease-in-out forwards",
  990. borderRadius: "4px",
  991. }}>
  992. <style>
  993. {`
  994. @keyframes fadeIn {
  995. from { opacity: 0; }
  996. to { opacity: 1.0; }
  997. }
  998. `}
  999. </style>
  1000. <VSCodeButton
  1001. className="copy-button"
  1002. appearance="icon"
  1003. style={{
  1004. height: "24px",
  1005. border: "none",
  1006. background: "var(--vscode-editor-background)",
  1007. transition: "background 0.2s ease-in-out",
  1008. }}
  1009. onClick={async () => {
  1010. const success = await copyWithFeedback(markdown)
  1011. if (success) {
  1012. const button = document.activeElement as HTMLElement
  1013. if (button) {
  1014. button.style.background = "var(--vscode-button-background)"
  1015. setTimeout(() => {
  1016. button.style.background = ""
  1017. }, 200)
  1018. }
  1019. }
  1020. }}
  1021. title="Copy as markdown">
  1022. <span className="codicon codicon-copy"></span>
  1023. </VSCodeButton>
  1024. </div>
  1025. )}
  1026. </div>
  1027. )
  1028. })