ChatRow.tsx 26 KB

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