Compare commits

...

6 Commits

Author SHA1 Message Date
celestial-vault 46ef339198 fix imports after merge conflicts 2025-07-14 16:58:07 -07:00
celestial-vault 964660892a merge conflicts 2025-07-14 16:57:45 -07:00
celestial-vault 2e33b042c8 Merge branch 'main' into markdown-mcp-responses 2025-07-14 16:50:52 -07:00
celestial-vault bca2fb4bf9 add display mode to global state; simplify state flow 2025-07-12 10:41:13 -07:00
celestial-vault c67cde2038 add markdown parsing to mcp response 2025-07-12 10:00:30 -07:00
celestial-vault 17fc8cf288 refactor out useEffect logic 2025-07-11 22:50:36 -07:00
11 changed files with 102 additions and 88 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ message UpdateSettingsRequest {
optional int64 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional bool mcp_rich_display_enabled = 11;
optional string mcp_display_mode = 11;
optional int64 terminal_output_line_limit = 12;
}
+2 -2
View File
@@ -834,7 +834,7 @@ export class Controller {
chatSettings: storedChatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
@@ -887,7 +887,7 @@ export class Controller {
chatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
+3 -3
View File
@@ -50,9 +50,9 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
}
// Update MCP responses collapsed setting
if (request.mcpRichDisplayEnabled !== undefined) {
await controller.context.globalState.update("mcpRichDisplayEnabled", request.mcpRichDisplayEnabled)
// Update MCP display mode setting
if (request.mcpDisplayMode !== undefined) {
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
}
// Update chat settings
+1 -1
View File
@@ -76,7 +76,7 @@ export type GlobalStateKey =
| "isNewUser"
| "welcomeViewCompleted"
| "terminalOutputLineLimit"
| "mcpRichDisplayEnabled"
| "mcpDisplayMode"
| "sapAiCoreTokenUrl"
| "sapAiCoreBaseUrl"
| "sapAiResourceGroup"
+4 -3
View File
@@ -11,6 +11,7 @@ import { StoredChatSettings } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { ClineRulesToggles } from "@shared/cline-rules"
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
/*
Storage
@@ -173,7 +174,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
mcpRichDisplayEnabled,
mcpDisplayMode,
mcpResponsesCollapsedRaw,
globalWorkflowToggles,
terminalReuseEnabled,
@@ -248,7 +249,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpRichDisplayEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpDisplayMode") as Promise<McpDisplayMode | undefined>,
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
@@ -470,7 +471,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
+2 -1
View File
@@ -7,6 +7,7 @@ import { HistoryItem } from "./HistoryItem"
import { TelemetrySetting } from "./TelemetrySetting"
import { ClineRulesToggles } from "./cline-rules"
import { UserInfo } from "./UserInfo"
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
// webview will hold state
export interface ExtensionMessage {
@@ -37,7 +38,7 @@ export interface ExtensionState {
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
mcpMarketplaceEnabled?: boolean
mcpRichDisplayEnabled: boolean
mcpDisplayMode: McpDisplayMode
planActSeparateModelsSetting: boolean
enableCheckpointsSetting?: boolean
platform: Platform
+9
View File
@@ -0,0 +1,9 @@
/**
* Represents the different display modes available for MCP responses
*/
export type McpDisplayMode = "rich" | "plain" | "markdown"
/**
* Default display mode for MCP responses
*/
export const DEFAULT_MCP_DISPLAY_MODE: McpDisplayMode = "plain"
@@ -0,0 +1,29 @@
import React from "react"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { McpDisplayMode } from "@shared/McpDisplayMode"
interface McpDisplayModeDropdownProps {
value: McpDisplayMode
onChange: (mode: McpDisplayMode) => void
id?: string
className?: string
style?: React.CSSProperties
onClick?: (e: React.MouseEvent) => void
}
const McpDisplayModeDropdown: React.FC<McpDisplayModeDropdownProps> = ({ value, onChange, id, className, style, onClick }) => {
const handleChange = (e: any) => {
const newMode = e.target.value as McpDisplayMode
onChange(newMode)
}
return (
<VSCodeDropdown id={id} value={value} onChange={handleChange} onClick={onClick} className={className} style={style}>
<VSCodeOption value="plain">Plain Text</VSCodeOption>
<VSCodeOption value="rich">Rich Display</VSCodeOption>
<VSCodeOption value="markdown">Markdown</VSCodeOption>
</VSCodeDropdown>
)
}
export default McpDisplayModeDropdown
@@ -1,11 +1,16 @@
import React, { useEffect, useState, useCallback } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import LinkPreview from "./LinkPreview"
import ImagePreview from "./ImagePreview"
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
import MarkdownBlock from "@/components/common/MarkdownBlock"
import McpDisplayModeDropdown from "./McpDisplayModeDropdown"
import { DropdownContainer } from "@/components/settings/ApiOptions"
import { updateSetting } from "@/components/settings/utils/settingsHandlers"
import { McpDisplayMode } from "@shared/McpDisplayMode"
import { UrlMatch, processResponseUrls, DisplaySegment, buildDisplaySegments } from "./utils/mcpRichUtil"
// Maximum number of URLs to process in total, per response
@@ -36,46 +41,6 @@ const ResponseHeader = styled.div`
}
`
const ToggleSwitch = styled.div`
display: flex;
align-items: center;
font-size: 12px;
color: var(--vscode-descriptionForeground);
.toggle-label {
margin-right: 8px;
}
.toggle-container {
position: relative;
width: 40px;
height: 20px;
background-color: var(--vscode-button-secondaryBackground);
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s;
}
.toggle-container.active {
background-color: var(--vscode-button-background);
}
.toggle-handle {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background-color: var(--vscode-button-foreground);
border-radius: 50%;
transition: transform 0.3s;
}
.toggle-container.active .toggle-handle {
transform: translateX(20px);
}
`
const ResponseContainer = styled.div`
position: relative;
font-family: var(--vscode-editor-font-family, monospace);
@@ -108,16 +73,15 @@ interface McpResponseDisplayProps {
}
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
const { mcpResponsesCollapsed, mcpRichDisplayEnabled } = useExtensionState() // Get setting from context
const { mcpResponsesCollapsed, mcpDisplayMode } = useExtensionState() // Get setting from context
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(mcpRichDisplayEnabled ? "rich" : "plain")
const [urlMatches, setUrlMatches] = useState<UrlMatch[]>([])
const [error, setError] = useState<string | null>(null)
const toggleDisplayMode = useCallback(() => {
setDisplayMode((prevMode) => (prevMode === "rich" ? "plain" : "rich"))
const handleDisplayModeChange = useCallback((newMode: McpDisplayMode) => {
updateSetting("mcpDisplayMode", newMode)
}, [])
const toggleExpand = useCallback(() => {
@@ -131,11 +95,11 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
// Find all URLs in the text and determine if they're images
useEffect(() => {
// Skip all processing if in plain mode
if (!isExpanded || displayMode === "plain") {
// Skip all processing if in plain mode or markdown mode
if (!isExpanded || mcpDisplayMode === "plain" || mcpDisplayMode === "markdown") {
setIsLoading(false)
if (urlMatches.length > 0) {
setUrlMatches([]) // Clear any existing matches when in plain mode
setUrlMatches([]) // Clear any existing matches when not in rich mode
}
return
}
@@ -162,10 +126,10 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
)
return cleanup
}, [responseText, displayMode, isExpanded])
}, [responseText, mcpDisplayMode, isExpanded])
// Helper function to render a display segment
const renderSegment = useCallback((segment: DisplaySegment): JSX.Element => {
const renderSegment = (segment: DisplaySegment): JSX.Element => {
switch (segment.type) {
case "text":
case "url":
@@ -205,7 +169,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
default:
return <React.Fragment key={segment.key} />
}
}, [])
}
// Function to render content based on display mode
const renderContent = () => {
@@ -213,7 +177,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
return null
}
if (isLoading && displayMode === "rich") {
if (isLoading && mcpDisplayMode === "rich") {
return (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
<VSCodeProgressRing />
@@ -221,10 +185,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
)
}
if (displayMode === "plain") {
if (mcpDisplayMode === "plain") {
return <UrlText>{responseText}</UrlText>
}
if (mcpDisplayMode === "markdown") {
return <MarkdownBlock markdown={responseText} />
}
if (error) {
return (
<>
@@ -234,7 +202,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
)
}
if (displayMode === "rich") {
if (mcpDisplayMode === "rich") {
const segments = buildDisplaySegments(responseText, urlMatches)
return <>{segments.map(renderSegment)}</>
}
@@ -255,16 +223,15 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response
</div>
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</div>
<DropdownContainer
style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<McpDisplayModeDropdown
value={mcpDisplayMode}
onChange={handleDisplayModeChange}
onClick={(e) => e.stopPropagation()}
style={{ minWidth: "120px" }}
/>
</DropdownContainer>
</ResponseHeader>
{isExpanded && <div className="response-content">{renderContent()}</div>}
@@ -4,6 +4,8 @@ import { memo } from "react"
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
import { updateSetting } from "../utils/settingsHandlers"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
import { McpDisplayMode } from "@shared/McpDisplayMode"
import McpDisplayModeDropdown from "@/components/mcp/chat-display/McpDisplayModeDropdown"
import Section from "../Section"
interface FeatureSettingsSectionProps {
@@ -11,7 +13,7 @@ interface FeatureSettingsSectionProps {
}
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpRichDisplayEnabled, mcpResponsesCollapsed, chatSettings } =
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpDisplayMode, mcpResponsesCollapsed, chatSettings } =
useExtensionState()
const handleReasoningEffortChange = (newValue: OpenAIReasoningEffort) => {
@@ -59,16 +61,20 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpRichDisplayEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
updateSetting("mcpRichDisplayEnabled", checked)
}}>
Enable Rich MCP Display
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables rich formatting for MCP responses. When disabled, responses will be shown in plain text.
<label
htmlFor="mcp-display-mode-dropdown"
className="block text-sm font-medium text-[var(--vscode-foreground)] mb-1">
MCP Display Mode
</label>
<McpDisplayModeDropdown
id="mcp-display-mode-dropdown"
value={mcpDisplayMode}
onChange={(newMode: McpDisplayMode) => updateSetting("mcpDisplayMode", newMode)}
className="w-full"
/>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Controls how MCP responses are displayed: plain text, rich formatting with links/images, or markdown
rendering.
</p>
</div>
<div style={{ marginTop: 10 }}>
@@ -31,6 +31,7 @@ import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/share
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
import { UserInfo } from "@shared/proto/account"
import { DEFAULT_MCP_DISPLAY_MODE } from "@shared/McpDisplayMode"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -189,7 +190,7 @@ export const ExtensionStateContextProvider: React.FC<{
distinctId: "",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
mcpRichDisplayEnabled: true,
mcpDisplayMode: DEFAULT_MCP_DISPLAY_MODE,
globalClineRulesToggles: {},
localClineRulesToggles: {},
localCursorRulesToggles: {},
@@ -762,7 +763,7 @@ export const ExtensionStateContextProvider: React.FC<{
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
mcpRichDisplayEnabled: state.mcpRichDisplayEnabled,
mcpDisplayMode: state.mcpDisplayMode,
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
}),
)