Skip to content

Commit fcaab09

Browse files
committed
fix: Markdown Tables and Styling Inconsistencies in Chat Messages
1 parent d3c65ce commit fcaab09

File tree

3 files changed

+130
-74
lines changed

3 files changed

+130
-74
lines changed

webview-ui/jest.config.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ module.exports = {
1717
"^\\./setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
1818
"^src/i18n/TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
1919
"^\\.\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
20-
"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx"
20+
"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
21+
'^react-markdown$': 'identity-obj-proxy',
22+
'^remark-gfm$': 'identity-obj-proxy',
23+
'^shiki$': 'identity-obj-proxy'
2124
},
2225
reporters: [["jest-simple-dot-reporter", {}]],
2326
transformIgnorePatterns: [

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 74 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import { vscode } from "../../utils/vscode"
1717
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
1818
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
1919
import CommandOutputViewer from "../common/CommandOutputViewer"
20-
import MarkdownBlock from "../common/MarkdownBlock"
2120
import { ReasoningBlock } from "./ReasoningBlock"
2221
import Thumbnails from "../common/Thumbnails"
2322
import McpResourceRow from "../mcp/McpResourceRow"
2423
import McpToolRow from "../mcp/McpToolRow"
2524
import { highlightMentions } from "./TaskHeader"
2625
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
2726
import FollowUpSuggest from "./FollowUpSuggest"
27+
import { Markdown } from "@/components/ui/markdown/Markdown"
2828

2929
interface ChatRowProps {
3030
message: ClineMessage
@@ -74,6 +74,71 @@ const ChatRow = memo(
7474

7575
export default ChatRow
7676

77+
// Define the new wrapper component with copy functionality
78+
const MarkdownWithCopy = memo(({ content, partial }: { content: string; partial?: boolean }) => {
79+
const [isHovering, setIsHovering] = useState(false)
80+
// Assuming useCopyToClipboard is imported correctly (it is, line 5)
81+
const { copyWithFeedback } = useCopyToClipboard(200) // Use shorter feedback duration like original
82+
83+
return (
84+
<div
85+
onMouseEnter={() => setIsHovering(true)}
86+
onMouseLeave={() => setIsHovering(false)}
87+
style={{ position: "relative" }}>
88+
{/* Apply negative margins and text wrap styles */}
89+
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
90+
{/* Use the imported shared Markdown component */}
91+
<Markdown content={content} />
92+
</div>
93+
{/* Conditional Copy Button */}
94+
{content && !partial && isHovering && (
95+
<div
96+
style={{
97+
position: "absolute",
98+
bottom: "-4px",
99+
right: "8px",
100+
opacity: 0,
101+
animation: "fadeIn 0.2s ease-in-out forwards",
102+
borderRadius: "4px",
103+
}}>
104+
<style>
105+
{`
106+
@keyframes fadeIn {
107+
from { opacity: 0; }
108+
to { opacity: 1.0; }
109+
}
110+
`}
111+
</style>
112+
<VSCodeButton
113+
className="copy-button"
114+
appearance="icon"
115+
style={{
116+
height: "24px",
117+
border: "none",
118+
background: "var(--vscode-editor-background)",
119+
transition: "background 0.2s ease-in-out",
120+
}}
121+
onClick={async () => {
122+
const success = await copyWithFeedback(content) // Use content prop
123+
if (success) {
124+
const button = document.activeElement as HTMLElement
125+
if (button) {
126+
button.style.background = "var(--vscode-button-background)"
127+
setTimeout(() => {
128+
button.style.background = ""
129+
}, 200)
130+
}
131+
}
132+
}}
133+
title="Copy as markdown">
134+
<span className="codicon codicon-copy"></span>
135+
</VSCodeButton>
136+
</div>
137+
)}
138+
</div>
139+
)
140+
})
141+
77142
export const ChatRowContent = ({
78143
message,
79144
lastModifiedMessage,
@@ -555,7 +620,7 @@ export const ChatRowContent = ({
555620
{t("chat:subtasks.newTaskContent")}
556621
</div>
557622
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
558-
<MarkdownBlock markdown={tool.content} />
623+
<Markdown content={tool.content || ""} />
559624
</div>
560625
</div>
561626
</>
@@ -592,7 +657,7 @@ export const ChatRowContent = ({
592657
{t("chat:subtasks.completionContent")}
593658
</div>
594659
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
595-
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
660+
<Markdown content={t("chat:subtasks.completionInstructions") || ""} />
596661
</div>
597662
</div>
598663
</>
@@ -729,7 +794,7 @@ export const ChatRowContent = ({
729794
padding: "12px 16px",
730795
backgroundColor: "var(--vscode-editor-background)",
731796
}}>
732-
<MarkdownBlock markdown={message.text} />
797+
<Markdown content={message.text || ""} />
733798
</div>
734799
</div>
735800
</div>
@@ -844,7 +909,7 @@ export const ChatRowContent = ({
844909
case "text":
845910
return (
846911
<div>
847-
<Markdown markdown={message.text} partial={message.partial} />
912+
<MarkdownWithCopy content={message.text || ""} partial={message.partial} />
848913
</div>
849914
)
850915
case "user_feedback":
@@ -932,7 +997,7 @@ export const ChatRowContent = ({
932997
{title}
933998
</div>
934999
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
935-
<Markdown markdown={message.text} />
1000+
<MarkdownWithCopy content={message.text || ""} partial={message.partial} />
9361001
</div>
9371002
</>
9381003
)
@@ -1023,7 +1088,7 @@ export const ChatRowContent = ({
10231088
</div>
10241089
)}
10251090
<div style={{ paddingTop: 10 }}>
1026-
<Markdown markdown={message.text} partial={message.partial} />
1091+
<MarkdownWithCopy content={message.text || ""} partial={message.partial} />
10271092
</div>
10281093
</>
10291094
)
@@ -1200,7 +1265,7 @@ export const ChatRowContent = ({
12001265
{title}
12011266
</div>
12021267
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
1203-
<Markdown markdown={message.text} partial={message.partial} />
1268+
<Markdown content={message.text || ""} />
12041269
</div>
12051270
</div>
12061271
)
@@ -1218,7 +1283,7 @@ export const ChatRowContent = ({
12181283
)}
12191284
<div style={{ paddingTop: 10, paddingBottom: 15 }}>
12201285
<Markdown
1221-
markdown={message.partial === true ? message?.text : followUpData?.question}
1286+
content={(message.partial === true ? message?.text : followUpData?.question) || ""}
12221287
/>
12231288
</div>
12241289
<FollowUpSuggest
@@ -1248,63 +1313,3 @@ export const ProgressIndicator = () => (
12481313
</div>
12491314
</div>
12501315
)
1251-
1252-
const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
1253-
const [isHovering, setIsHovering] = useState(false)
1254-
const { copyWithFeedback } = useCopyToClipboard(200) // shorter feedback duration for copy button flash
1255-
1256-
return (
1257-
<div
1258-
onMouseEnter={() => setIsHovering(true)}
1259-
onMouseLeave={() => setIsHovering(false)}
1260-
style={{ position: "relative" }}>
1261-
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
1262-
<MarkdownBlock markdown={markdown} />
1263-
</div>
1264-
{markdown && !partial && isHovering && (
1265-
<div
1266-
style={{
1267-
position: "absolute",
1268-
bottom: "-4px",
1269-
right: "8px",
1270-
opacity: 0,
1271-
animation: "fadeIn 0.2s ease-in-out forwards",
1272-
borderRadius: "4px",
1273-
}}>
1274-
<style>
1275-
{`
1276-
@keyframes fadeIn {
1277-
from { opacity: 0; }
1278-
to { opacity: 1.0; }
1279-
}
1280-
`}
1281-
</style>
1282-
<VSCodeButton
1283-
className="copy-button"
1284-
appearance="icon"
1285-
style={{
1286-
height: "24px",
1287-
border: "none",
1288-
background: "var(--vscode-editor-background)",
1289-
transition: "background 0.2s ease-in-out",
1290-
}}
1291-
onClick={async () => {
1292-
const success = await copyWithFeedback(markdown)
1293-
if (success) {
1294-
const button = document.activeElement as HTMLElement
1295-
if (button) {
1296-
button.style.background = "var(--vscode-button-background)"
1297-
setTimeout(() => {
1298-
button.style.background = ""
1299-
}, 200)
1300-
}
1301-
}
1302-
}}
1303-
title="Copy as markdown">
1304-
<span className="codicon codicon-copy"></span>
1305-
</VSCodeButton>
1306-
</div>
1307-
)}
1308-
</div>
1309-
)
1310-
})

webview-ui/src/components/ui/markdown/Markdown.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FC, memo } from "react"
22
import ReactMarkdown, { Options } from "react-markdown"
33
import remarkGfm from "remark-gfm"
4+
import { cn } from "@/lib/utils"
45

56
import { Separator } from "@/components/ui"
67

@@ -16,7 +17,7 @@ export function Markdown({ content }: { content: string }) {
1617
return (
1718
<MemoizedReactMarkdown
1819
remarkPlugins={[remarkGfm]}
19-
className="custom-markdown break-words"
20+
className="custom-markdown break-words text-[var(--vscode-font-size)]"
2021
components={{
2122
p({ children }) {
2223
return <div className="mb-2 last:mb-0">{children}</div>
@@ -26,14 +27,14 @@ export function Markdown({ content }: { content: string }) {
2627
},
2728
ol({ children }) {
2829
return (
29-
<ol className="list-decimal pl-4 [&>li]:mb-1 [&>li:last-child]:mb-0 [&>li>ul]:mt-1 [&>li>ol]:mt-1">
30+
<ol className="list-decimal pl-[2.5em] [&>li]:mb-1 [&>li:last-child]:mb-0 [&>li>ul]:mt-1 [&>li>ol]:mt-1">
3031
{children}
3132
</ol>
3233
)
3334
},
3435
ul({ children }) {
3536
return (
36-
<ul className="list-disc pl-4 [&>li]:mb-1 [&>li:last-child]:mb-0 [&>li>ul]:mt-1 [&>li>ol]:mt-1">
37+
<ul className="list-disc pl-[2.5em] [&>li]:mb-1 [&>li:last-child]:mb-0 [&>li>ul]:mt-1 [&>li>ol]:mt-1">
3738
{children}
3839
</ul>
3940
)
@@ -56,7 +57,12 @@ export function Markdown({ content }: { content: string }) {
5657
props.node?.position && props.node.position.start.line === props.node.position.end.line
5758

5859
return isInline ? (
59-
<code className={className} {...props}>
60+
<code
61+
className={cn(
62+
"font-mono bg-[var(--vscode-textCodeBlock-background)] text-muted-foreground px-1 py-[0.1em] rounded-sm border border-border inline-block align-baseline",
63+
className, // Allow original className (like language-) to be passed if needed, though less likely for inline
64+
)}
65+
{...props}>
6066
{children}
6167
</code>
6268
) : (
@@ -67,6 +73,48 @@ export function Markdown({ content }: { content: string }) {
6773
/>
6874
)
6975
},
76+
table({ children }) {
77+
// Use w-full for full width, border-collapse for clean borders,
78+
// border and border-border for VS Code theme-aware borders, my-2 for margin
79+
return <table className="w-full my-2 border-collapse border border-border">{children}</table>
80+
},
81+
thead({ children }) {
82+
// Add bottom border and a subtle background consistent with muted elements
83+
return <thead className="border-b border-border bg-muted/50">{children}</thead>
84+
},
85+
tbody({ children }) {
86+
// No specific styling needed for tbody itself
87+
return <tbody>{children}</tbody>
88+
},
89+
tr({ children }) {
90+
// Add bottom border, hover effect, and remove border for the last row
91+
return (
92+
<tr className="border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted last:border-b-0">
93+
{children}
94+
</tr>
95+
)
96+
},
97+
th({ children }) {
98+
// Add right border, padding, left alignment, medium font weight, muted text color,
99+
// and remove border for the last header cell
100+
return (
101+
<th className="border-r border-border p-2 text-left align-middle font-medium text-muted-foreground last:border-r-0">
102+
{children}
103+
</th>
104+
)
105+
},
106+
strong({ children }) {
107+
// Apply a slightly lighter font weight than default bold
108+
return <strong className="font-semibold">{children}</strong>
109+
},
110+
td({ children }) {
111+
// Add right border, padding, left alignment, and remove border for the last data cell
112+
return (
113+
<td className="border-r border-border p-2 text-left align-middle last:border-r-0">
114+
{children}
115+
</td>
116+
)
117+
},
70118
a({ href, children }) {
71119
return (
72120
<a href={href} target="_blank" rel="noopener noreferrer">

0 commit comments

Comments
 (0)