Skip to content

Commit 996a7cd

Browse files
committed
fix: improve JSON parsing logic and handle partial responses in McpExecution component
1 parent 1492e57 commit 996a7cd

File tree

1 file changed

+77
-40
lines changed

1 file changed

+77
-40
lines changed

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

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const McpExecution = ({
5353
const [isResponseExpanded, setIsResponseExpanded] = useState(false)
5454

5555
// Try to parse JSON and return both the result and formatted text
56-
const tryParseJson = (text: string): { isJson: boolean; formatted: string } => {
56+
const tryParseJson = useCallback((text: string): { isJson: boolean; formatted: string } => {
5757
if (!text) return { isJson: false, formatted: "" }
5858

5959
try {
@@ -68,10 +68,51 @@ export const McpExecution = ({
6868
formatted: text,
6969
}
7070
}
71-
}
71+
}, [])
72+
73+
// Only parse response data when expanded AND complete to avoid parsing partial JSON
74+
const responseData = useMemo(() => {
75+
if (!isResponseExpanded) {
76+
return { isJson: false, formatted: responseText }
77+
}
78+
// Only try to parse JSON if the response is complete
79+
if (status && status.status === "completed") {
80+
return tryParseJson(responseText)
81+
}
82+
// For partial responses, just return as-is without parsing
83+
return { isJson: false, formatted: responseText }
84+
}, [responseText, isResponseExpanded, tryParseJson, status])
85+
86+
// Only parse arguments data when complete to avoid parsing partial JSON
87+
const argumentsData = useMemo(() => {
88+
if (!argumentsText) {
89+
return { isJson: false, formatted: "" }
90+
}
91+
92+
// For arguments, we don't have a streaming status, so we check if it looks like complete JSON
93+
const trimmed = argumentsText.trim()
94+
95+
// Basic check for complete JSON structure
96+
if (
97+
trimmed &&
98+
((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")))
99+
) {
100+
// Try to parse, but if it fails, return as-is
101+
try {
102+
const parsed = JSON.parse(trimmed)
103+
return {
104+
isJson: true,
105+
formatted: JSON.stringify(parsed, null, 2),
106+
}
107+
} catch {
108+
// JSON structure looks complete but is invalid, return as-is
109+
return { isJson: false, formatted: argumentsText }
110+
}
111+
}
72112

73-
const responseData = useMemo(() => tryParseJson(responseText), [responseText])
74-
const argumentsData = useMemo(() => tryParseJson(argumentsText), [argumentsText])
113+
// For non-JSON or incomplete data, just return as-is
114+
return { isJson: false, formatted: argumentsText }
115+
}, [argumentsText])
75116

76117
const formattedResponseText = responseData.formatted
77118
const formattedArgumentsText = argumentsData.formatted
@@ -99,16 +140,8 @@ export const McpExecution = ({
99140

100141
if (data.status === "output" && data.response) {
101142
setResponseText((prev) => prev + data.response)
102-
// Keep the arguments when we get output
103-
if (isArguments && argumentsText === responseText) {
104-
setArgumentsText(responseText)
105-
}
106143
} else if (data.status === "completed" && data.response) {
107144
setResponseText(data.response)
108-
// Keep the arguments when we get completed response
109-
if (isArguments && argumentsText === responseText) {
110-
setArgumentsText(responseText)
111-
}
112145
}
113146
}
114147
}
@@ -117,30 +150,16 @@ export const McpExecution = ({
117150
}
118151
}
119152
},
120-
[argumentsText, executionId, isArguments, responseText],
153+
[executionId],
121154
)
122155

123156
useEvent("message", onMessage)
124157

125158
// Initialize with text if provided and parse command/response sections
126159
useEffect(() => {
127-
// Handle arguments text
160+
// Handle arguments text - don't parse JSON here as it might be incomplete
128161
if (text) {
129-
try {
130-
// Try to parse the text as JSON for arguments
131-
const jsonObj = safeJsonParse<any>(text, null)
132-
133-
if (jsonObj && typeof jsonObj === "object") {
134-
// Format the JSON for display
135-
setArgumentsText(JSON.stringify(jsonObj, null, 2))
136-
} else {
137-
// If not valid JSON, use as is
138-
setArgumentsText(text)
139-
}
140-
} catch (_e) {
141-
// If parsing fails, use text as is
142-
setArgumentsText(text)
143-
}
162+
setArgumentsText(text)
144163
}
145164

146165
// Handle response text
@@ -258,6 +277,7 @@ export const McpExecution = ({
258277
response={formattedResponseText}
259278
isJson={responseIsJson}
260279
hasArguments={!!(isArguments || useMcpServer?.arguments || argumentsText)}
280+
isPartial={status ? status.status !== "completed" : false}
261281
/>
262282
</div>
263283
</>
@@ -271,21 +291,38 @@ const ResponseContainerInternal = ({
271291
response,
272292
isJson,
273293
hasArguments,
294+
isPartial = false,
274295
}: {
275296
isExpanded: boolean
276297
response: string
277298
isJson: boolean
278299
hasArguments?: boolean
279-
}) => (
280-
<div
281-
className={cn("overflow-hidden", {
282-
"max-h-0": !isExpanded,
283-
"max-h-[100%] mt-1 pt-1 border-t border-border/25": isExpanded && hasArguments,
284-
"max-h-[100%] mt-1 pt-1": isExpanded && !hasArguments,
285-
})}>
286-
{response.length > 0 &&
287-
(isJson ? <CodeBlock source={response} language="json" /> : <Markdown markdown={response} partial={false} />)}
288-
</div>
289-
)
300+
isPartial?: boolean
301+
}) => {
302+
// Only render content when expanded to prevent performance issues with large responses
303+
if (!isExpanded || response.length === 0) {
304+
return (
305+
<div
306+
className={cn("overflow-hidden", {
307+
"max-h-0": !isExpanded,
308+
})}
309+
/>
310+
)
311+
}
312+
313+
return (
314+
<div
315+
className={cn("overflow-hidden", {
316+
"max-h-[100%] mt-1 pt-1 border-t border-border/25": hasArguments,
317+
"max-h-[100%] mt-1 pt-1": !hasArguments,
318+
})}>
319+
{isJson ? (
320+
<CodeBlock source={response} language="json" />
321+
) : (
322+
<Markdown markdown={response} partial={isPartial} />
323+
)}
324+
</div>
325+
)
326+
}
290327

291328
const ResponseContainer = memo(ResponseContainerInternal)

0 commit comments

Comments
 (0)