Skip to content

Commit 582a3b1

Browse files
valinha0xToshii
andauthored
Collapsible Panel for MCP Responses (RooCodeInc#3528)
* feat: the response of the mcps is represented by a collapsible * feat: the response of the mcps is represented by a collapsible * fix: message for error parsing response * fix: format * feat: added cline.mcp.defaultPanelState settings * chore: merge * feat: improve global state * chore: re-trigger workflow * chore: lint fix * revert: unnecessary changes * Update webview-ui/src/components/mcp/chat-display/McpResponseDisplay.tsx Co-authored-by: Toshii <[email protected]> * chore: change MCP Response Display Mode to a button * fix: improve ui mcp response panel * refactor: change name and type for property mcpDefaultPanelState to mcpResponsesCollapsed * chore: rename props --------- Co-authored-by: Toshii <[email protected]>
1 parent 8c8a398 commit 582a3b1

File tree

10 files changed

+121
-26
lines changed

10 files changed

+121
-26
lines changed

.changeset/smart-ladybugs-give.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
the response of the mcps is displayed with a collapsible which allows to focus on the model responses.

src/core/controller/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,10 @@ export class Controller {
328328
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
329329
}
330330

331+
if (typeof message.mcpResponsesCollapsed === "boolean") {
332+
await updateGlobalState(this.context, "mcpResponsesCollapsed", message.mcpResponsesCollapsed)
333+
}
334+
331335
// chat settings (including preferredLanguage and openAIReasoningEffort)
332336
if (message.chatSettings) {
333337
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
@@ -1100,6 +1104,7 @@ export class Controller {
11001104
shellIntegrationTimeout,
11011105
terminalReuseEnabled,
11021106
isNewUser,
1107+
mcpResponsesCollapsed,
11031108
} = await getAllExtensionState(this.context)
11041109

11051110
const localClineRulesToggles =
@@ -1145,6 +1150,7 @@ export class Controller {
11451150
shellIntegrationTimeout,
11461151
terminalReuseEnabled,
11471152
isNewUser,
1153+
mcpResponsesCollapsed,
11481154
}
11491155
}
11501156

src/core/storage/state-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type GlobalStateKey =
9191
| "favoritedModelIds"
9292
| "requestTimeoutMs"
9393
| "shellIntegrationTimeout"
94+
| "mcpResponsesCollapsed"
9495
| "terminalReuseEnabled"
9596
| "isNewUser"
9697

src/core/storage/state.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
164164
shellIntegrationTimeout,
165165
enableCheckpointsSettingRaw,
166166
mcpMarketplaceEnabledRaw,
167+
mcpResponsesCollapsedRaw,
167168
globalWorkflowToggles,
168169
terminalReuseEnabled,
169170
] = await Promise.all([
@@ -255,6 +256,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
255256
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
256257
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
257258
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
259+
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
258260
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
259261
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
260262
])
@@ -277,6 +279,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
277279

278280
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
279281
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
282+
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
280283

281284
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
282285
// On win11 state sometimes initializes as empty string instead of undefined
@@ -387,6 +390,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
387390
previousModeAwsBedrockCustomSelected,
388391
previousModeAwsBedrockCustomModelBaseId,
389392
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
393+
mcpResponsesCollapsed: mcpResponsesCollapsed,
390394
telemetrySetting: telemetrySetting || "unset",
391395
planActSeparateModelsSetting,
392396
enableCheckpointsSetting: enableCheckpointsSetting,

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export interface ExtensionState {
123123
globalWorkflowToggles: ClineRulesToggles
124124
localCursorRulesToggles: ClineRulesToggles
125125
localWindsurfRulesToggles: ClineRulesToggles
126+
mcpResponsesCollapsed?: boolean
126127
}
127128

128129
export interface ClineMessage {

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface WebviewMessage {
5252
planActSeparateModelsSetting?: boolean
5353
enableCheckpointsSetting?: boolean
5454
mcpMarketplaceEnabled?: boolean
55+
mcpResponsesCollapsed?: boolean
5556
telemetrySetting?: TelemetrySetting
5657
customInstructionsSetting?: string
5758
mentionsRequestId?: string

webview-ui/src/components/mcp/chat-display/McpResponseDisplay.tsx

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import React, { useEffect, useState, useCallback } from "react"
2+
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
3+
import { useExtensionState } from "../../../context/ExtensionStateContext"
24
import LinkPreview from "./LinkPreview"
35
import ImagePreview from "./ImagePreview"
46
import styled from "styled-components"
@@ -28,6 +30,10 @@ const ResponseHeader = styled.div`
2830
text-overflow: ellipsis;
2931
margin-right: 8px;
3032
}
33+
34+
.header-icon {
35+
margin-right: 6px;
36+
}
3137
`
3238

3339
const ToggleSwitch = styled.div`
@@ -111,7 +117,9 @@ interface UrlMatch {
111117
}
112118

113119
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
114-
const [isLoading, setIsLoading] = useState(true)
120+
const { mcpResponsesCollapsed } = useExtensionState() // Get setting from context
121+
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
122+
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
115123
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
116124
// Get saved preference from localStorage, default to 'rich'
117125
const savedMode = localStorage.getItem("mcpDisplayMode")
@@ -124,41 +132,46 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
124132

125133
const toggleDisplayMode = useCallback(() => {
126134
const newMode = displayMode === "rich" ? "plain" : "rich"
127-
128135
// Force an immediate re-render
129136
setForceUpdateCounter((prev) => prev + 1)
130-
131137
// Update display mode and save preference
132138
setDisplayMode(newMode)
133139
localStorage.setItem("mcpDisplayMode", newMode)
134-
135140
// If switching to plain mode, cancel any ongoing processing
136141
if (newMode === "plain") {
137142
console.log("Switching to plain mode - cancelling URL processing")
138143
setUrlMatches([]) // Clear any existing matches when switching to plain mode
139144
} else {
140145
// If switching to rich mode, the useEffect will re-run and fetch data
141146
console.log("Switching to rich mode - will start URL processing")
147+
setUrlMatches([])
142148
}
143149
}, [displayMode])
144150

151+
const toggleExpand = useCallback(() => {
152+
setIsExpanded((prev) => !prev)
153+
}, [])
154+
155+
// Effect to update isExpanded if mcpResponsesCollapsed changes from context
156+
useEffect(() => {
157+
setIsExpanded(!mcpResponsesCollapsed)
158+
}, [])
159+
145160
// Find all URLs in the text and determine if they're images
146161
useEffect(() => {
147162
// Skip all processing if in plain mode
148-
if (displayMode === "plain") {
163+
if (!isExpanded || displayMode === "plain") {
149164
setIsLoading(false)
150165
setUrlMatches([]) // Clear any existing matches when in plain mode
151166
return
152167
}
153168

154169
// Use a direct boolean for cancellation that's scoped to this effect run
155170
let processingCanceled = false
156-
157171
const processResponse = async () => {
158172
console.log("Processing MCP response for URL extraction")
159173
setIsLoading(true)
160174
setError(null)
161-
162175
try {
163176
const text = responseText || ""
164177
const matches: UrlMatch[] = []
@@ -267,12 +280,24 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
267280
processingCanceled = true
268281
console.log("Cleaning up URL processing")
269282
}
270-
}, [responseText, displayMode, forceUpdateCounter])
283+
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
271284

272285
// Function to render content based on display mode
273286
const renderContent = () => {
287+
if (!isExpanded) {
288+
return null // Don't render content if not expanded
289+
}
290+
291+
if (isLoading && displayMode === "rich") {
292+
return (
293+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
294+
<VSCodeProgressRing />
295+
</div>
296+
)
297+
}
298+
274299
// For plain text mode, just show the text
275-
if (displayMode === "plain" || isLoading) {
300+
if (displayMode === "plain") {
276301
return <UrlText>{responseText}</UrlText>
277302
}
278303

@@ -287,7 +312,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
287312
}
288313

289314
// For rich display mode, show the text with embedded content
290-
if (!isLoading) {
315+
if (displayMode === "rich") {
291316
// We already know displayMode is "rich" if we get here
292317
// Create an array of text segments and embedded content
293318
const segments: JSX.Element[] = []
@@ -385,30 +410,48 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
385410
try {
386411
return (
387412
<ResponseContainer>
388-
<ResponseHeader>
389-
<span className="header-title">Response</span>
390-
<ToggleSwitch>
391-
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
392-
<div className={`toggle-container ${displayMode === "rich" ? "active" : ""}`} onClick={toggleDisplayMode}>
393-
<div className="toggle-handle"></div>
394-
</div>
395-
</ToggleSwitch>
413+
<ResponseHeader
414+
onClick={toggleExpand}
415+
style={{
416+
borderBottom: isExpanded ? "1px dashed var(--vscode-editorGroup-border)" : "none",
417+
marginBottom: isExpanded ? "8px" : "0px",
418+
}}>
419+
<div className="header-title">
420+
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
421+
Response
422+
</div>
423+
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
424+
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
425+
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
426+
<div
427+
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
428+
onClick={toggleDisplayMode}>
429+
<div className="toggle-handle"></div>
430+
</div>
431+
</ToggleSwitch>
432+
</div>
396433
</ResponseHeader>
397434

398-
<div className="response-content">{renderContent()}</div>
435+
{isExpanded && <div className="response-content">{renderContent()}</div>}
399436
</ResponseContainer>
400437
)
401438
} catch (error) {
402-
console.log("Error rendering MCP response - falling back to plain text")
439+
console.log("Error rendering MCP response - falling back to plain text") // Restored comment
440+
// Fallback for critical rendering errors
403441
return (
404442
<ResponseContainer>
405-
<ResponseHeader>
406-
<span className="header-title">Response</span>
443+
<ResponseHeader onClick={toggleExpand}>
444+
<div className="header-title">
445+
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
446+
Response (Error)
447+
</div>
407448
</ResponseHeader>
408-
<div className="response-content">
409-
<div>Error parsing response:</div>
410-
<UrlText>{responseText}</UrlText>
411-
</div>
449+
{isExpanded && (
450+
<div className="response-content">
451+
<div style={{ color: "var(--vscode-errorForeground)" }}>Error parsing response:</div>
452+
<UrlText>{responseText}</UrlText>
453+
</div>
454+
)}
412455
</ResponseContainer>
413456
)
414457
}

webview-ui/src/components/settings/FeatureSettingsSection.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const FeatureSettingsSection = () => {
99
setEnableCheckpointsSetting,
1010
mcpMarketplaceEnabled,
1111
setMcpMarketplaceEnabled,
12+
mcpResponsesCollapsed,
13+
setMcpResponsesCollapsed,
1214
chatSettings,
1315
setChatSettings,
1416
} = useExtensionState()
@@ -42,6 +44,19 @@ const FeatureSettingsSection = () => {
4244
Enables the MCP Marketplace tab for discovering and installing MCP servers.
4345
</p>
4446
</div>
47+
<div style={{ marginTop: 10 }}>
48+
<VSCodeCheckbox
49+
checked={mcpResponsesCollapsed}
50+
onChange={(e: any) => {
51+
const checked = e.target.checked === true
52+
setMcpResponsesCollapsed(checked)
53+
}}>
54+
Collapse MCP Responses
55+
</VSCodeCheckbox>
56+
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
57+
Sets the default display mode for MCP response panels
58+
</p>
59+
</div>
4560
<div style={{ marginTop: 10 }}>
4661
<label
4762
htmlFor="openai-reasoning-effort-dropdown"

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
130130
setShellIntegrationTimeout,
131131
terminalReuseEnabled,
132132
setTerminalReuseEnabled,
133+
mcpResponsesCollapsed,
134+
setMcpResponsesCollapsed,
133135
setApiConfiguration,
134136
} = useExtensionState()
135137

@@ -141,6 +143,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
141143
planActSeparateModelsSetting,
142144
enableCheckpointsSetting,
143145
mcpMarketplaceEnabled,
146+
mcpResponsesCollapsed,
144147
chatSettings,
145148
shellIntegrationTimeout,
146149
terminalReuseEnabled,
@@ -186,6 +189,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
186189
mcpMarketplaceEnabled,
187190
shellIntegrationTimeout,
188191
terminalReuseEnabled,
192+
mcpResponsesCollapsed,
189193
apiConfiguration: apiConfigurationToSubmit,
190194
})
191195

@@ -208,6 +212,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
208212
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
209213
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
210214
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
215+
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
211216
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
212217
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
213218
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
@@ -220,6 +225,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
220225
planActSeparateModelsSetting,
221226
enableCheckpointsSetting,
222227
mcpMarketplaceEnabled,
228+
mcpResponsesCollapsed,
223229
chatSettings,
224230
shellIntegrationTimeout,
225231
terminalReuseEnabled,
@@ -260,6 +266,9 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
260266
if (typeof setTerminalReuseEnabled === "function") {
261267
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
262268
}
269+
if (typeof setMcpResponsesCollapsed === "function") {
270+
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
271+
}
263272
// Close settings view
264273
onDone()
265274
}
@@ -277,6 +286,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
277286
setApiConfiguration,
278287
setEnableCheckpointsSetting,
279288
setMcpMarketplaceEnabled,
289+
setMcpResponsesCollapsed,
280290
])
281291

282292
// Handle confirmation dialog actions

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface ExtensionStateContextType extends ExtensionState {
5252
setPlanActSeparateModelsSetting: (value: boolean) => void
5353
setEnableCheckpointsSetting: (value: boolean) => void
5454
setMcpMarketplaceEnabled: (value: boolean) => void
55+
setMcpResponsesCollapsed: (value: boolean) => void
5556
setShellIntegrationTimeout: (value: number) => void
5657
setTerminalReuseEnabled: (value: boolean) => void
5758
setChatSettings: (value: ChatSettings) => void
@@ -179,6 +180,7 @@ export const ExtensionStateContextProvider: React.FC<{
179180
shellIntegrationTimeout: 4000, // default timeout for shell integration
180181
terminalReuseEnabled: true, // default to enabled for backward compatibility
181182
isNewUser: false,
183+
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
182184
})
183185
const [didHydrateState, setDidHydrateState] = useState(false)
184186
const [showWelcome, setShowWelcome] = useState(false)
@@ -622,6 +624,12 @@ export const ExtensionStateContextProvider: React.FC<{
622624
...prevState,
623625
mcpMarketplaceEnabled: value,
624626
})),
627+
setMcpResponsesCollapsed: (value) => {
628+
setState((prevState) => ({
629+
...prevState,
630+
mcpResponsesCollapsed: value,
631+
}))
632+
},
625633
setShowAnnouncement,
626634
setShouldShowAnnouncement: (value) =>
627635
setState((prevState) => ({
@@ -656,6 +664,7 @@ export const ExtensionStateContextProvider: React.FC<{
656664
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
657665
enableCheckpointsSetting: state.enableCheckpointsSetting,
658666
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
667+
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
659668
})
660669
},
661670
setGlobalClineRulesToggles: (toggles) =>

0 commit comments

Comments
 (0)