ChatRow.tsx 33 KB

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