ChatRow.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
  2. import deepEqual from "fast-deep-equal"
  3. import React, { memo, useMemo } from "react"
  4. import ReactMarkdown from "react-markdown"
  5. import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
  6. import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
  7. import CodeAccordian, { formatFilePathForTruncation } from "./CodeAccordian"
  8. import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
  9. import Thumbnails from "./Thumbnails"
  10. import { vscode } from "../utils/vscode"
  11. import { highlightMentions } from "./TaskHeader"
  12. interface ChatRowProps {
  13. message: ClaudeMessage
  14. isExpanded: boolean
  15. onToggleExpand: () => void
  16. lastModifiedMessage?: ClaudeMessage
  17. isLast: boolean
  18. }
  19. const ChatRow = memo(
  20. (props: ChatRowProps) => {
  21. // 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
  22. return (
  23. <div
  24. style={{
  25. padding: "10px 6px 10px 15px",
  26. }}>
  27. <ChatRowContent {...props} />
  28. </div>
  29. )
  30. },
  31. // memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
  32. deepEqual
  33. )
  34. export default ChatRow
  35. const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowProps) => {
  36. const cost = useMemo(() => {
  37. if (message.text != null && message.say === "api_req_started") {
  38. return JSON.parse(message.text).cost
  39. }
  40. return undefined
  41. }, [message.text, message.say])
  42. const apiRequestFailedMessage =
  43. isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
  44. ? lastModifiedMessage?.text
  45. : undefined
  46. const isCommandExecuting =
  47. isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
  48. const type = message.type === "ask" ? message.ask : message.say
  49. const normalColor = "var(--vscode-foreground)"
  50. const errorColor = "var(--vscode-errorForeground)"
  51. const successColor = "var(--vscode-charts-green)"
  52. const [icon, title] = useMemo(() => {
  53. switch (type) {
  54. case "error":
  55. return [
  56. <span
  57. className="codicon codicon-error"
  58. style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
  59. <span style={{ color: errorColor, fontWeight: "bold" }}>Error</span>,
  60. ]
  61. case "mistake_limit_reached":
  62. return [
  63. <span
  64. className="codicon codicon-error"
  65. style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
  66. <span style={{ color: errorColor, fontWeight: "bold" }}>Claude is having trouble...</span>,
  67. ]
  68. case "command":
  69. return [
  70. isCommandExecuting ? (
  71. <ProgressIndicator />
  72. ) : (
  73. <span
  74. className="codicon codicon-terminal"
  75. style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
  76. ),
  77. <span style={{ color: normalColor, fontWeight: "bold" }}>
  78. Claude wants to execute this command:
  79. </span>,
  80. ]
  81. case "completion_result":
  82. return [
  83. <span
  84. className="codicon codicon-check"
  85. style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
  86. <span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
  87. ]
  88. case "api_req_started":
  89. return [
  90. cost != null ? (
  91. <span
  92. className="codicon codicon-check"
  93. style={{ color: successColor, marginBottom: "-1.5px" }}></span>
  94. ) : apiRequestFailedMessage ? (
  95. <span
  96. className="codicon codicon-error"
  97. style={{ color: errorColor, marginBottom: "-1.5px" }}></span>
  98. ) : (
  99. <ProgressIndicator />
  100. ),
  101. cost != null ? (
  102. <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
  103. ) : apiRequestFailedMessage ? (
  104. <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
  105. ) : (
  106. <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
  107. ),
  108. ]
  109. case "followup":
  110. return [
  111. <span
  112. className="codicon codicon-question"
  113. style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
  114. <span style={{ color: normalColor, fontWeight: "bold" }}>Claude has a question:</span>,
  115. ]
  116. default:
  117. return [null, null]
  118. }
  119. }, [type, cost, apiRequestFailedMessage, isCommandExecuting])
  120. const headerStyle: React.CSSProperties = {
  121. display: "flex",
  122. alignItems: "center",
  123. gap: "10px",
  124. marginBottom: "10px",
  125. }
  126. const pStyle: React.CSSProperties = {
  127. margin: 0,
  128. whiteSpace: "pre-wrap",
  129. wordBreak: "break-word",
  130. overflowWrap: "anywhere",
  131. }
  132. const tool = useMemo(() => {
  133. if (message.ask === "tool" || message.say === "tool") {
  134. return JSON.parse(message.text || "{}") as ClaudeSayTool
  135. }
  136. return null
  137. }, [message.ask, message.say, message.text])
  138. if (tool) {
  139. const toolIcon = (name: string) => (
  140. <span
  141. className={`codicon codicon-${name}`}
  142. style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
  143. )
  144. switch (tool.tool) {
  145. case "editedExistingFile":
  146. return (
  147. <>
  148. <div style={headerStyle}>
  149. {toolIcon("edit")}
  150. <span style={{ fontWeight: "bold" }}>Claude wants to edit this file:</span>
  151. </div>
  152. <CodeAccordian
  153. diff={tool.diff!}
  154. path={tool.path!}
  155. isExpanded={isExpanded}
  156. onToggleExpand={onToggleExpand}
  157. />
  158. </>
  159. )
  160. case "newFileCreated":
  161. return (
  162. <>
  163. <div style={headerStyle}>
  164. {toolIcon("new-file")}
  165. <span style={{ fontWeight: "bold" }}>Claude wants to create a new file:</span>
  166. </div>
  167. <CodeAccordian
  168. code={tool.content!}
  169. path={tool.path!}
  170. isExpanded={isExpanded}
  171. onToggleExpand={onToggleExpand}
  172. />
  173. </>
  174. )
  175. case "readFile":
  176. return (
  177. <>
  178. <div style={headerStyle}>
  179. {toolIcon("file-code")}
  180. <span style={{ fontWeight: "bold" }}>
  181. {message.type === "ask" ? "Claude wants to read this file:" : "Claude read this file:"}
  182. </span>
  183. </div>
  184. {/* <CodeAccordian
  185. code={tool.content!}
  186. path={tool.path!}
  187. isExpanded={isExpanded}
  188. onToggleExpand={onToggleExpand}
  189. /> */}
  190. <div
  191. style={{
  192. borderRadius: 3,
  193. backgroundColor: CODE_BLOCK_BG_COLOR,
  194. overflow: "hidden",
  195. border: "1px solid var(--vscode-editorGroup-border)",
  196. }}>
  197. <div
  198. style={{
  199. color: "var(--vscode-descriptionForeground)",
  200. display: "flex",
  201. justifyContent: "space-between",
  202. alignItems: "center",
  203. padding: "9px 10px",
  204. cursor: "pointer",
  205. userSelect: "none",
  206. WebkitUserSelect: "none",
  207. MozUserSelect: "none",
  208. msUserSelect: "none",
  209. }}
  210. onClick={() => {
  211. vscode.postMessage({ type: "openFile", text: tool.content })
  212. }}>
  213. <span
  214. style={{
  215. whiteSpace: "nowrap",
  216. overflow: "hidden",
  217. textOverflow: "ellipsis",
  218. marginRight: "8px",
  219. direction: "rtl",
  220. textAlign: "left",
  221. unicodeBidi: "plaintext",
  222. }}>
  223. {formatFilePathForTruncation(tool.path ?? "") + "\u200E"}
  224. </span>
  225. <span
  226. className={`codicon codicon-link-external`}
  227. style={{ fontSize: 13.5, margin: "1px 0" }}></span>
  228. </div>
  229. </div>
  230. </>
  231. )
  232. case "listFilesTopLevel":
  233. return (
  234. <>
  235. <div style={headerStyle}>
  236. {toolIcon("folder-opened")}
  237. <span style={{ fontWeight: "bold" }}>
  238. {message.type === "ask"
  239. ? "Claude wants to view the top level files in this directory:"
  240. : "Claude viewed the top level files in this directory:"}
  241. </span>
  242. </div>
  243. <CodeAccordian
  244. code={tool.content!}
  245. path={tool.path!}
  246. language="shell-session"
  247. isExpanded={isExpanded}
  248. onToggleExpand={onToggleExpand}
  249. />
  250. </>
  251. )
  252. case "listFilesRecursive":
  253. return (
  254. <>
  255. <div style={headerStyle}>
  256. {toolIcon("folder-opened")}
  257. <span style={{ fontWeight: "bold" }}>
  258. {message.type === "ask"
  259. ? "Claude wants to recursively view all files in this directory:"
  260. : "Claude recursively viewed all files in this directory:"}
  261. </span>
  262. </div>
  263. <CodeAccordian
  264. code={tool.content!}
  265. path={tool.path!}
  266. language="shell-session"
  267. isExpanded={isExpanded}
  268. onToggleExpand={onToggleExpand}
  269. />
  270. </>
  271. )
  272. case "listCodeDefinitionNames":
  273. return (
  274. <>
  275. <div style={headerStyle}>
  276. {toolIcon("file-code")}
  277. <span style={{ fontWeight: "bold" }}>
  278. {message.type === "ask"
  279. ? "Claude wants to view source code definition names used in this directory:"
  280. : "Claude viewed source code definition names used in this directory:"}
  281. </span>
  282. </div>
  283. <CodeAccordian
  284. code={tool.content!}
  285. path={tool.path!}
  286. isExpanded={isExpanded}
  287. onToggleExpand={onToggleExpand}
  288. />
  289. </>
  290. )
  291. case "searchFiles":
  292. return (
  293. <>
  294. <div style={headerStyle}>
  295. {toolIcon("search")}
  296. <span style={{ fontWeight: "bold" }}>
  297. {message.type === "ask" ? (
  298. <>
  299. Claude wants to search this directory for <code>{tool.regex}</code>:
  300. </>
  301. ) : (
  302. <>
  303. Claude searched this directory for <code>{tool.regex}</code>:
  304. </>
  305. )}
  306. </span>
  307. </div>
  308. <CodeAccordian
  309. code={tool.content!}
  310. path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
  311. language="plaintext"
  312. isExpanded={isExpanded}
  313. onToggleExpand={onToggleExpand}
  314. />
  315. </>
  316. )
  317. default:
  318. return null
  319. }
  320. }
  321. switch (message.type) {
  322. case "say":
  323. switch (message.say) {
  324. case "api_req_started":
  325. return (
  326. <>
  327. <div
  328. style={{
  329. ...headerStyle,
  330. marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
  331. justifyContent: "space-between",
  332. cursor: "pointer",
  333. userSelect: "none",
  334. WebkitUserSelect: "none",
  335. MozUserSelect: "none",
  336. msUserSelect: "none",
  337. }}
  338. onClick={onToggleExpand}>
  339. <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
  340. {icon}
  341. {title}
  342. {cost != null && cost > 0 && <VSCodeBadge>${Number(cost)?.toFixed(4)}</VSCodeBadge>}
  343. </div>
  344. <span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
  345. </div>
  346. {cost == null && apiRequestFailedMessage && (
  347. <>
  348. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
  349. {apiRequestFailedMessage}
  350. {apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
  351. <>
  352. <br />
  353. <br />
  354. It seems like you're having Windows PowerShell issues, please see this{" "}
  355. <a
  356. href="https://github.com/saoudrizwan/claude-dev/wiki/TroubleShooting-%E2%80%90-Windows-PowerShell-Errors"
  357. style={{ color: "inherit", textDecoration: "underline" }}>
  358. troubleshooting guide
  359. </a>
  360. .
  361. </>
  362. )}
  363. </p>
  364. {/* {apiProvider === "kodu" && (
  365. <div
  366. style={{
  367. display: "flex",
  368. alignItems: "center",
  369. backgroundColor:
  370. "color-mix(in srgb, var(--vscode-errorForeground) 20%, transparent)",
  371. color: "var(--vscode-editor-foreground)",
  372. padding: "6px 8px",
  373. borderRadius: "3px",
  374. margin: "10px 0 0 0",
  375. fontSize: "12px",
  376. }}>
  377. <i
  378. className="codicon codicon-warning"
  379. style={{
  380. marginRight: 6,
  381. fontSize: 16,
  382. color: "var(--vscode-errorForeground)",
  383. }}></i>
  384. <span>
  385. Uh-oh, this could be a problem on Kodu's end. We've been alerted and
  386. will resolve this ASAP. You can also{" "}
  387. <a
  388. href="https://discord.gg/claudedev"
  389. style={{ color: "inherit", textDecoration: "underline" }}>
  390. contact us on discord
  391. </a>
  392. .
  393. </span>
  394. </div>
  395. )} */}
  396. </>
  397. )}
  398. {isExpanded && (
  399. <div style={{ marginTop: "10px" }}>
  400. <CodeAccordian
  401. code={JSON.parse(message.text || "{}").request}
  402. language="markdown"
  403. isExpanded={true}
  404. onToggleExpand={onToggleExpand}
  405. />
  406. </div>
  407. )}
  408. </>
  409. )
  410. case "api_req_finished":
  411. return null // we should never see this message type
  412. case "text":
  413. return (
  414. <div>
  415. <Markdown markdown={message.text} />
  416. </div>
  417. )
  418. case "user_feedback":
  419. return (
  420. <div
  421. style={{
  422. backgroundColor: "var(--vscode-badge-background)",
  423. color: "var(--vscode-badge-foreground)",
  424. borderRadius: "3px",
  425. padding: "9px",
  426. whiteSpace: "pre-line",
  427. wordWrap: "break-word",
  428. }}>
  429. <span style={{ display: "block" }}>{highlightMentions(message.text)}</span>
  430. {message.images && message.images.length > 0 && (
  431. <Thumbnails images={message.images} style={{ marginTop: "8px" }} />
  432. )}
  433. </div>
  434. )
  435. case "user_feedback_diff":
  436. const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
  437. return (
  438. <div
  439. style={{
  440. marginTop: -10,
  441. width: "100%",
  442. }}>
  443. <CodeAccordian
  444. diff={tool.diff!}
  445. isFeedback={true}
  446. isExpanded={isExpanded}
  447. onToggleExpand={onToggleExpand}
  448. />
  449. </div>
  450. )
  451. case "error":
  452. return (
  453. <>
  454. {title && (
  455. <div style={headerStyle}>
  456. {icon}
  457. {title}
  458. </div>
  459. )}
  460. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
  461. </>
  462. )
  463. case "completion_result":
  464. return (
  465. <>
  466. <div style={headerStyle}>
  467. {icon}
  468. {title}
  469. </div>
  470. <div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
  471. <Markdown markdown={message.text} />
  472. </div>
  473. </>
  474. )
  475. case "shell_integration_warning":
  476. return (
  477. <>
  478. <div
  479. style={{
  480. display: "flex",
  481. flexDirection: "column",
  482. backgroundColor: "rgba(255, 191, 0, 0.1)",
  483. padding: 8,
  484. borderRadius: 3,
  485. fontSize: 12,
  486. }}>
  487. <div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
  488. <i
  489. className="codicon codicon-warning"
  490. style={{
  491. marginRight: 8,
  492. fontSize: 18,
  493. color: "#FFA500",
  494. }}></i>
  495. <span style={{ fontWeight: 500, color: "#FFA500" }}>
  496. Shell Integration Unavailable
  497. </span>
  498. </div>
  499. <div>
  500. Claude won't be able to view the command's output. Please update VSCode (
  501. <code>CMD/CTRL + Shift + P</code> → "Update") and make sure you're using a supported
  502. shell: zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> →
  503. "Terminal: Select Default Profile").{" "}
  504. <a
  505. href="https://github.com/saoudrizwan/claude-dev/wiki/Troubleshooting-%E2%80%90-Shell-Integration-Unavailable"
  506. style={{ color: "inherit", textDecoration: "underline" }}>
  507. Still having trouble?
  508. </a>
  509. </div>
  510. </div>
  511. </>
  512. )
  513. default:
  514. return (
  515. <>
  516. {title && (
  517. <div style={headerStyle}>
  518. {icon}
  519. {title}
  520. </div>
  521. )}
  522. <div style={{ paddingTop: 10 }}>
  523. <Markdown markdown={message.text} />
  524. </div>
  525. </>
  526. )
  527. }
  528. case "ask":
  529. switch (message.ask) {
  530. case "mistake_limit_reached":
  531. return (
  532. <>
  533. <div style={headerStyle}>
  534. {icon}
  535. {title}
  536. </div>
  537. <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
  538. </>
  539. )
  540. case "command":
  541. const splitMessage = (text: string) => {
  542. const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
  543. if (outputIndex === -1) {
  544. return { command: text, output: "" }
  545. }
  546. return {
  547. command: text.slice(0, outputIndex).trim(),
  548. output: text
  549. .slice(outputIndex + COMMAND_OUTPUT_STRING.length)
  550. .trim()
  551. .split("")
  552. .map((char) => {
  553. switch (char) {
  554. case "\t":
  555. return "→ "
  556. case "\b":
  557. return "⌫"
  558. case "\f":
  559. return "⏏"
  560. case "\v":
  561. return "⇳"
  562. default:
  563. return char
  564. }
  565. })
  566. .join(""),
  567. }
  568. }
  569. const { command, output } = splitMessage(message.text || "")
  570. return (
  571. <>
  572. <div style={headerStyle}>
  573. {icon}
  574. {title}
  575. </div>
  576. {/* <Terminal
  577. rawOutput={command + (output ? "\n" + output : "")}
  578. shouldAllowInput={!!isCommandExecuting && output.length > 0}
  579. /> */}
  580. <div
  581. style={{
  582. borderRadius: 3,
  583. border: "1px solid var(--vscode-editorGroup-border)",
  584. overflow: "hidden",
  585. backgroundColor: CODE_BLOCK_BG_COLOR,
  586. }}>
  587. <CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
  588. {output.length > 0 && (
  589. <div style={{ width: "100%" }}>
  590. <div
  591. onClick={onToggleExpand}
  592. style={{
  593. display: "flex",
  594. alignItems: "center",
  595. gap: "4px",
  596. width: "100%",
  597. justifyContent: "flex-start",
  598. cursor: "pointer",
  599. padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
  600. }}>
  601. <span
  602. className={`codicon codicon-chevron-${
  603. isExpanded ? "down" : "right"
  604. }`}></span>
  605. <span style={{ fontSize: "0.8em" }}>Command Output</span>
  606. </div>
  607. {isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
  608. </div>
  609. )}
  610. </div>
  611. </>
  612. )
  613. case "completion_result":
  614. if (message.text) {
  615. return (
  616. <div>
  617. <div style={headerStyle}>
  618. {icon}
  619. {title}
  620. </div>
  621. <div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
  622. <Markdown markdown={message.text} />
  623. </div>
  624. </div>
  625. )
  626. } else {
  627. return null // Don't render anything when we get a completion_result ask without text
  628. }
  629. case "followup":
  630. return (
  631. <>
  632. {title && (
  633. <div style={headerStyle}>
  634. {icon}
  635. {title}
  636. </div>
  637. )}
  638. <div style={{ paddingTop: 10 }}>
  639. <Markdown markdown={message.text} />
  640. </div>
  641. </>
  642. )
  643. default:
  644. return null
  645. }
  646. }
  647. }
  648. const ProgressIndicator = () => (
  649. <div
  650. style={{
  651. width: "16px",
  652. height: "16px",
  653. display: "flex",
  654. alignItems: "center",
  655. justifyContent: "center",
  656. }}>
  657. <div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
  658. <VSCodeProgressRing />
  659. </div>
  660. </div>
  661. )
  662. const Markdown = memo(({ markdown }: { markdown?: string }) => {
  663. // react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
  664. // when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly
  665. const parsed = markdown?.replace(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
  666. return content
  667. // return `_<thinking>_\n\n${content}\n\n_</thinking>_`
  668. })
  669. return (
  670. <div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -10, marginTop: -10 }}>
  671. <ReactMarkdown
  672. children={parsed}
  673. components={{
  674. p(props) {
  675. const { style, ...rest } = props
  676. return (
  677. <p
  678. style={{
  679. ...style,
  680. margin: 0,
  681. marginTop: 10,
  682. marginBottom: 10,
  683. whiteSpace: "pre-wrap",
  684. wordBreak: "break-word",
  685. overflowWrap: "anywhere",
  686. }}
  687. {...rest}
  688. />
  689. )
  690. },
  691. ol(props) {
  692. const { style, ...rest } = props
  693. return (
  694. <ol
  695. style={{
  696. ...style,
  697. padding: "0 0 0 20px",
  698. margin: "10px 0",
  699. wordBreak: "break-word",
  700. overflowWrap: "anywhere",
  701. }}
  702. {...rest}
  703. />
  704. )
  705. },
  706. ul(props) {
  707. const { style, ...rest } = props
  708. return (
  709. <ul
  710. style={{
  711. ...style,
  712. padding: "0 0 0 20px",
  713. margin: "10px 0",
  714. wordBreak: "break-word",
  715. overflowWrap: "anywhere",
  716. }}
  717. {...rest}
  718. />
  719. )
  720. },
  721. // pre always surrounds a code, and we custom handle code blocks below. Pre has some non-10 margin, while all other elements in markdown have a 10 top/bottom margin and the outer div has a -10 top/bottom margin to counteract this between chat rows. However we render markdown in a completion_result row so make sure to add padding as necessary when used within other rows.
  722. pre(props) {
  723. const { style, ...rest } = props
  724. return (
  725. <pre
  726. style={{
  727. ...style,
  728. marginTop: 10,
  729. marginBlock: 10,
  730. }}
  731. {...rest}
  732. />
  733. )
  734. },
  735. // https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
  736. code(props) {
  737. const { children, className, node, ...rest } = props
  738. const match = /language-(\w+)/.exec(className || "")
  739. return match ? (
  740. <div
  741. style={{
  742. borderRadius: 3,
  743. border: "1px solid var(--vscode-editorGroup-border)",
  744. overflow: "hidden",
  745. }}>
  746. <CodeBlock
  747. source={`${"```"}${match[1]}\n${String(children).replace(/\n$/, "")}\n${"```"}`}
  748. />
  749. </div>
  750. ) : (
  751. <code
  752. {...rest}
  753. className={className}
  754. style={{
  755. whiteSpace: "pre-line",
  756. wordBreak: "break-word",
  757. overflowWrap: "anywhere",
  758. backgroundColor: "var(--vscode-textCodeBlock-background)",
  759. color: "var(--vscode-textPreformat-foreground)",
  760. fontFamily: "var(--vscode-editor-font-family)",
  761. fontSize: "var(--vscode-editor-font-size)",
  762. borderRadius: "3px",
  763. border: "1px solid var(--vscode-textSeparator-foreground)",
  764. // padding: "2px 4px",
  765. }}>
  766. {children}
  767. </code>
  768. )
  769. },
  770. }}
  771. />
  772. </div>
  773. )
  774. })