mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee27da2e28 | |||
| c9b922009f | |||
| 2d6ff38e69 | |||
| 3069e27413 | |||
| 57c8b8120d | |||
| 5243f0b9b1 | |||
| c014060275 | |||
| d790ce86a0 | |||
| 5e2b199377 | |||
| 9234d0cdc4 | |||
| db1db8c95d | |||
| f53af72643 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Pass linter diagnostics with the read_file tool
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Kimi-K2 as the trending model in the Cline Provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Added API Key support for Bedrock integration
|
||||
+10
-1
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.1]
|
||||
|
||||
- Fix documentation
|
||||
|
||||
## [3.19.0]
|
||||
|
||||
- Add Kimi-K2 as a recommended model in the Cline Provider, and route to Together/Groq for 131k context window and high throughput
|
||||
- Added API Key support for Bedrock integration
|
||||
|
||||
## [3.18.14]
|
||||
|
||||
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
|
||||
@@ -40,7 +49,7 @@
|
||||
## [3.18.6]
|
||||
|
||||
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
|
||||
- Add organization organization accounts
|
||||
- Add organization accounts
|
||||
|
||||
## [3.18.5]
|
||||
|
||||
|
||||
+2
-2
@@ -153,8 +153,8 @@ const extensionConfig = {
|
||||
// Standalone-specific configuration
|
||||
const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/standalone.ts"],
|
||||
outfile: `${destDir}/standalone.js`,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.18.14",
|
||||
"version": "3.19.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,31 @@ DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
|
||||
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
|
||||
TMP=/tmp/vscode-sdk-uses.txt.tmp
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
|
||||
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
|
||||
sort | uniq -c | sort -n | # Count occurrences
|
||||
cat > $SDK_DEST
|
||||
cat > $TMP
|
||||
}
|
||||
{
|
||||
grep -rh vscode.commands.executeCommand $DIR |
|
||||
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
|
||||
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
|
||||
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
|
||||
cat >> $TMP
|
||||
}
|
||||
|
||||
# Count occurrences
|
||||
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
|
||||
rm $TMP
|
||||
|
||||
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
|
||||
|
||||
{
|
||||
|
||||
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
|
||||
shouldApplyMiddleOutTransform = true
|
||||
}
|
||||
|
||||
// hardcoded provider sorting for kimi-k2
|
||||
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
|
||||
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
// limit providers to only those that support the 131k context window
|
||||
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -104,6 +104,12 @@ export async function refreshOpenRouterModels(
|
||||
modelInfo.cacheWritesPrice = 0.75
|
||||
modelInfo.cacheReadsPrice = 0
|
||||
break
|
||||
case "moonshotai/kimi-k2":
|
||||
// forcing kimi-k2 to use the together provider for full context and best throughput
|
||||
modelInfo.inputPrice = 1
|
||||
modelInfo.outputPrice = 3
|
||||
modelInfo.contextWindow = 131_000
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,7 +76,7 @@ export type GlobalStateKey =
|
||||
| "isNewUser"
|
||||
| "welcomeViewCompleted"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "mcpDisplayMode"
|
||||
| "sapAiCoreTokenUrl"
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -41,7 +41,6 @@ import os from "os"
|
||||
import * as path from "path"
|
||||
import { serializeError } from "serialize-error"
|
||||
import * as vscode from "vscode"
|
||||
import { diagnosticsToProblemsString } from "@integrations/diagnostics"
|
||||
import { ToolResponse, USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "."
|
||||
import { ToolParamName, ToolUse, ToolUseName } from "../assistant-message"
|
||||
import { constructNewFileContent } from "../assistant-message/diff"
|
||||
@@ -840,28 +839,10 @@ export class ToolExecutor {
|
||||
// now execute the tool like normal
|
||||
const content = await extractTextFromFile(absolutePath)
|
||||
|
||||
// Get diagnostics for this specific file only
|
||||
const fileUri = vscode.Uri.file(absolutePath)
|
||||
const fileDiagnostics = vscode.languages.getDiagnostics(fileUri)
|
||||
|
||||
// Format diagnostics if any exist
|
||||
let diagnosticsMessage = ""
|
||||
if (fileDiagnostics.length > 0) {
|
||||
const problemsString = await diagnosticsToProblemsString(
|
||||
[[fileUri, fileDiagnostics]],
|
||||
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
|
||||
)
|
||||
|
||||
diagnosticsMessage = `\n\n---\nNOTE: This file has linter issues. Only address these if they're relevant to your current task:\n${problemsString}\n---`
|
||||
}
|
||||
|
||||
// Combine content with diagnostics
|
||||
const finalContent = content + diagnosticsMessage
|
||||
|
||||
// Track file read operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
this.pushToolResult(finalContent, block)
|
||||
this.pushToolResult(content, block)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cline-standalone",
|
||||
"name": "cline-core",
|
||||
"version": "0.0.1",
|
||||
"main": "standalone.js",
|
||||
"main": "cline-core.js",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
|
||||
@@ -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,12 +1,17 @@
|
||||
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 { isUrl, isLocalhostUrl, formatUrlForOpening, checkIfImageUrl } from "./utils/mcpRichUtil"
|
||||
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
|
||||
export const MAX_URLS = 50
|
||||
@@ -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);
|
||||
@@ -107,28 +72,16 @@ interface McpResponseDisplayProps {
|
||||
responseText: string
|
||||
}
|
||||
|
||||
// Represents a URL found in the text with its position and metadata
|
||||
interface UrlMatch {
|
||||
url: string // The actual URL
|
||||
fullMatch: string // The full matched text
|
||||
index: number // Position in the text
|
||||
isImage: boolean // Whether this URL is an image
|
||||
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
|
||||
}
|
||||
|
||||
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">(() => {
|
||||
// Initialize directly from the global setting.
|
||||
return 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(() => {
|
||||
@@ -142,138 +95,89 @@ 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
|
||||
}
|
||||
|
||||
// Use a direct boolean for cancellation that's scoped to this effect run
|
||||
let processingCanceled = false
|
||||
const processResponse = async () => {
|
||||
console.log("Processing MCP response for URL extraction")
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const text = responseText || ""
|
||||
const matches: UrlMatch[] = []
|
||||
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
|
||||
let urlMatch: RegExpExecArray | null
|
||||
let urlCount = 0
|
||||
console.log("Processing MCP response for URL extraction")
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
// First pass: Extract all URLs and immediately make them available for rendering
|
||||
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < MAX_URLS) {
|
||||
// Get the original URL from the match - never modify the original URL text
|
||||
const url = urlMatch[0]
|
||||
|
||||
// Skip invalid URLs
|
||||
if (!isUrl(url)) {
|
||||
console.log("Skipping invalid URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip localhost URLs to prevent security issues
|
||||
if (isLocalhostUrl(url)) {
|
||||
console.log("Skipping localhost URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
matches.push({
|
||||
url,
|
||||
fullMatch: url,
|
||||
index: urlMatch.index,
|
||||
isImage: false, // Will check later
|
||||
isProcessed: false,
|
||||
})
|
||||
|
||||
urlCount++
|
||||
}
|
||||
|
||||
console.log(`Found ${matches.length} URLs in text, will check if they are images`)
|
||||
|
||||
// Set matches immediately so UI can start rendering with loading states
|
||||
setUrlMatches(matches.sort((a, b) => a.index - b.index))
|
||||
|
||||
// Mark loading as complete to show content immediately
|
||||
// Use the orchestrator function from mcpRichUtil
|
||||
const cleanup = processResponseUrls(
|
||||
responseText || "",
|
||||
MAX_URLS,
|
||||
(matches) => {
|
||||
setUrlMatches(matches)
|
||||
setIsLoading(false)
|
||||
|
||||
// Process image checks in the background - one at a time to avoid network flooding
|
||||
const processImageChecks = async () => {
|
||||
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
// Skip already processed URLs (from extension check)
|
||||
if (matches[i].isProcessed) continue
|
||||
|
||||
// Check if processing has been canceled (switched to plain mode)
|
||||
if (processingCanceled) {
|
||||
console.log("URL processing canceled - display mode changed to plain")
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[i]
|
||||
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
|
||||
|
||||
try {
|
||||
// Process each URL individually
|
||||
const isImage = await checkIfImageUrl(match.url)
|
||||
|
||||
// Skip if processing has been canceled
|
||||
if (processingCanceled) return
|
||||
|
||||
// Update the match in place
|
||||
match.isImage = isImage
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state after each URL to show progress
|
||||
// Create a new array to ensure React detects the state change
|
||||
setUrlMatches([...matches])
|
||||
} catch (err) {
|
||||
console.log(`URL check error: ${match.url}`, err)
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state even on error
|
||||
if (!processingCanceled) {
|
||||
setUrlMatches([...matches])
|
||||
}
|
||||
}
|
||||
|
||||
// Delay between URL processing to avoid overwhelming the network
|
||||
if (!processingCanceled && i < matches.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
|
||||
}
|
||||
|
||||
// Start the background processing
|
||||
processImageChecks()
|
||||
} catch (error) {
|
||||
setError("Failed to process response content. Switch to plain text mode to view safely.")
|
||||
},
|
||||
(updatedMatches) => {
|
||||
setUrlMatches(updatedMatches)
|
||||
},
|
||||
(errorMessage) => {
|
||||
setError(errorMessage)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
processResponse()
|
||||
return cleanup
|
||||
}, [responseText, mcpDisplayMode, isExpanded])
|
||||
|
||||
// Cleanup function to cancel processing if component unmounts or dependencies change
|
||||
return () => {
|
||||
processingCanceled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
// Helper function to render a display segment
|
||||
const renderSegment = (segment: DisplaySegment): JSX.Element => {
|
||||
switch (segment.type) {
|
||||
case "text":
|
||||
case "url":
|
||||
return <UrlText key={segment.key}>{segment.content}</UrlText>
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<div key={segment.key}>
|
||||
<ImagePreview url={segment.url!} />
|
||||
</div>
|
||||
)
|
||||
|
||||
case "link":
|
||||
return (
|
||||
<div key={segment.key} style={{ margin: "10px 0" }}>
|
||||
<LinkPreview url={segment.url!} />
|
||||
</div>
|
||||
)
|
||||
|
||||
case "error":
|
||||
return (
|
||||
<div
|
||||
key={segment.key}
|
||||
style={{
|
||||
margin: "10px 0",
|
||||
padding: "8px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
border: "1px solid var(--vscode-editorError-foreground)",
|
||||
borderRadius: "4px",
|
||||
height: "128px",
|
||||
overflow: "auto",
|
||||
}}>
|
||||
{segment.content}
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return <React.Fragment key={segment.key} />
|
||||
}
|
||||
}, [responseText, displayMode, isExpanded])
|
||||
}
|
||||
|
||||
// Function to render content based on display mode
|
||||
const renderContent = () => {
|
||||
if (!isExpanded) {
|
||||
return null // Don't render content if not expanded
|
||||
return null
|
||||
}
|
||||
|
||||
if (isLoading && displayMode === "rich") {
|
||||
if (isLoading && mcpDisplayMode === "rich") {
|
||||
return (
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
|
||||
<VSCodeProgressRing />
|
||||
@@ -281,12 +185,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
)
|
||||
}
|
||||
|
||||
// For plain text mode, just show the text
|
||||
if (displayMode === "plain") {
|
||||
if (mcpDisplayMode === "plain") {
|
||||
return <UrlText>{responseText}</UrlText>
|
||||
}
|
||||
|
||||
// Show error message if there was an error
|
||||
if (mcpDisplayMode === "markdown") {
|
||||
return <MarkdownBlock markdown={responseText} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
@@ -296,97 +202,9 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
|
||||
)
|
||||
}
|
||||
|
||||
// For rich display mode, show the text with embedded content
|
||||
if (displayMode === "rich") {
|
||||
// We already know displayMode is "rich" if we get here
|
||||
// Create an array of text segments and embedded content
|
||||
const segments: JSX.Element[] = []
|
||||
let lastIndex = 0
|
||||
let segmentIndex = 0
|
||||
|
||||
// Track embed count for logging
|
||||
let embedCount = 0
|
||||
|
||||
// Add the text before the first URL
|
||||
if (urlMatches.length === 0) {
|
||||
segments.push(<UrlText key={`segment-${segmentIndex}`}>{responseText}</UrlText>)
|
||||
} else {
|
||||
for (let i = 0; i < urlMatches.length; i++) {
|
||||
const match = urlMatches[i]
|
||||
const { url, fullMatch, index } = match
|
||||
|
||||
// Add text segment before this URL
|
||||
if (index > lastIndex) {
|
||||
segments.push(
|
||||
<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex, index)}</UrlText>,
|
||||
)
|
||||
}
|
||||
|
||||
// Add the URL text itself
|
||||
segments.push(<UrlText key={`url-${segmentIndex++}`}>{fullMatch}</UrlText>)
|
||||
|
||||
// Calculate the end position of this URL in the text
|
||||
const urlEndIndex = index + fullMatch.length
|
||||
|
||||
// Add embedded content after the URL
|
||||
// For images, use the ImagePreview component
|
||||
if (match.isImage) {
|
||||
segments.push(
|
||||
<div key={`embed-image-${url}-${segmentIndex++}`}>
|
||||
{/* Use formatUrlForOpening for network calls but preserve original URL in display */}
|
||||
<ImagePreview url={formatUrlForOpening(url)} />
|
||||
</div>,
|
||||
)
|
||||
embedCount++
|
||||
// console.log(`Added image embed for ${url}, embed count: ${embedCount}`);
|
||||
} else if (match.isProcessed) {
|
||||
// For non-image URLs or URLs we haven't processed yet, show link preview
|
||||
try {
|
||||
// Skip localhost URLs
|
||||
if (!isLocalhostUrl(url)) {
|
||||
// Use a unique key that includes the URL to ensure each preview is isolated
|
||||
segments.push(
|
||||
<div key={`embed-${url}-${segmentIndex++}`} style={{ margin: "10px 0" }}>
|
||||
{/* Already using formatUrlForOpening for link previews */}
|
||||
<LinkPreview url={formatUrlForOpening(url)} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
embedCount++
|
||||
// console.log(`Added link preview for ${url}, embed count: ${embedCount}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Link preview could not be created")
|
||||
// Show error message for failed link preview
|
||||
segments.push(
|
||||
<div
|
||||
key={`embed-error-${segmentIndex++}`}
|
||||
style={{
|
||||
margin: "10px 0",
|
||||
padding: "8px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
border: "1px solid var(--vscode-editorError-foreground)",
|
||||
borderRadius: "4px",
|
||||
height: "128px", // Fixed height
|
||||
overflow: "auto", // Allow scrolling if content overflows
|
||||
}}>
|
||||
Failed to create preview for: {url}
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update lastIndex for next segment
|
||||
lastIndex = urlEndIndex
|
||||
}
|
||||
|
||||
// Add any remaining text after the last URL
|
||||
if (lastIndex < responseText.length) {
|
||||
segments.push(<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex)}</UrlText>)
|
||||
}
|
||||
}
|
||||
|
||||
return <>{segments}</>
|
||||
if (mcpDisplayMode === "rich") {
|
||||
const segments = buildDisplaySegments(responseText, urlMatches)
|
||||
return <>{segments.map(renderSegment)}</>
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -405,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>}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import { WebServiceClient } from "@/services/grpc-client"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
|
||||
// Represents a URL found in the text with its position and metadata
|
||||
export interface UrlMatch {
|
||||
url: string // The actual URL
|
||||
fullMatch: string // The full matched text
|
||||
index: number // Position in the text
|
||||
isImage: boolean // Whether this URL is an image
|
||||
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
|
||||
}
|
||||
|
||||
// Display segment interface
|
||||
export interface DisplaySegment {
|
||||
type: "text" | "url" | "image" | "link" | "error"
|
||||
content: string
|
||||
url?: string
|
||||
key: string // Pre-computed key for React
|
||||
}
|
||||
|
||||
// Safely create a URL object with error handling and ensure HTTPS
|
||||
export const safeCreateUrl = (url: string): URL | null => {
|
||||
try {
|
||||
@@ -168,3 +185,224 @@ export const checkIfImageUrl = async (url: string): Promise<boolean> => {
|
||||
console.log(`URL protocol not supported for image check: ${url}`)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all valid URLs from the given text
|
||||
* @param text - The text to search for URLs
|
||||
* @param maxUrls - Maximum number of URLs to extract (default: 50)
|
||||
* @returns Array of URL matches sorted by position in text
|
||||
*/
|
||||
export const extractUrlsFromText = (text: string, maxUrls: number = 50): UrlMatch[] => {
|
||||
const matches: UrlMatch[] = []
|
||||
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
|
||||
let urlMatch: RegExpExecArray | null
|
||||
let urlCount = 0
|
||||
|
||||
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < maxUrls) {
|
||||
const url = urlMatch[0]
|
||||
|
||||
// Skip invalid URLs
|
||||
if (!isUrl(url)) {
|
||||
console.log("Skipping invalid URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip localhost URLs to prevent security issues
|
||||
if (isLocalhostUrl(url)) {
|
||||
console.log("Skipping localhost URL:", url)
|
||||
continue
|
||||
}
|
||||
|
||||
matches.push({
|
||||
url,
|
||||
fullMatch: url,
|
||||
index: urlMatch.index,
|
||||
isImage: false, // Will be determined later
|
||||
isProcessed: false,
|
||||
})
|
||||
|
||||
urlCount++
|
||||
}
|
||||
|
||||
console.log(`Found ${matches.length} URLs in text`)
|
||||
return matches.sort((a, b) => a.index - b.index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes URLs to determine their types (e.g., image vs link)
|
||||
* Processes URLs sequentially to avoid network flooding
|
||||
* @param matches - Array of URL matches to process
|
||||
* @param onProgress - Callback for progress updates with updated matches
|
||||
* @param cancellationToken - Object to check if processing should be cancelled
|
||||
* @returns Promise that resolves when processing is complete
|
||||
*/
|
||||
export const processUrlTypes = async (
|
||||
matches: UrlMatch[],
|
||||
onProgress: (updatedMatches: UrlMatch[]) => void,
|
||||
cancellationToken: { cancelled: boolean },
|
||||
): Promise<void> => {
|
||||
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
// Skip already processed URLs
|
||||
if (matches[i].isProcessed) continue
|
||||
|
||||
// Check if processing has been canceled
|
||||
if (cancellationToken.cancelled) {
|
||||
console.log("URL processing canceled")
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[i]
|
||||
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
|
||||
|
||||
try {
|
||||
// Check if URL is an image
|
||||
const isImage = await checkIfImageUrl(match.url)
|
||||
|
||||
// Skip if processing has been canceled
|
||||
if (cancellationToken.cancelled) return
|
||||
|
||||
// Update the match
|
||||
match.isImage = isImage
|
||||
match.isProcessed = true
|
||||
|
||||
// Notify progress with a new array to ensure React detects changes
|
||||
onProgress([...matches])
|
||||
} catch (err) {
|
||||
console.log(`URL check error: ${match.url}`, err)
|
||||
match.isProcessed = true
|
||||
|
||||
// Update state even on error
|
||||
if (!cancellationToken.cancelled) {
|
||||
onProgress([...matches])
|
||||
}
|
||||
}
|
||||
|
||||
// Delay between URL processing to avoid overwhelming the network
|
||||
if (!cancellationToken.cancelled && i < matches.length - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates the URL extraction and processing pipeline
|
||||
* @param text - The response text to process
|
||||
* @param maxUrls - Maximum number of URLs to process
|
||||
* @param onMatchesFound - Callback when initial URLs are extracted
|
||||
* @param onMatchesUpdated - Callback when URL types are determined
|
||||
* @param onError - Error handler callback
|
||||
* @returns Cleanup function to cancel processing
|
||||
*/
|
||||
export const processResponseUrls = (
|
||||
text: string,
|
||||
maxUrls: number,
|
||||
onMatchesFound: (matches: UrlMatch[]) => void,
|
||||
onMatchesUpdated: (matches: UrlMatch[]) => void,
|
||||
onError: (error: string) => void,
|
||||
): (() => void) => {
|
||||
const cancellationToken = { cancelled: false }
|
||||
|
||||
const process = async () => {
|
||||
try {
|
||||
// Extract URLs from text
|
||||
const matches = extractUrlsFromText(text, maxUrls)
|
||||
|
||||
// Immediately notify about found matches
|
||||
onMatchesFound(matches)
|
||||
|
||||
// Process URLs in the background
|
||||
await processUrlTypes(matches, onMatchesUpdated, cancellationToken)
|
||||
} catch (error) {
|
||||
onError("Failed to process response content. Switch to plain text mode to view safely.")
|
||||
}
|
||||
}
|
||||
|
||||
// Start processing
|
||||
process()
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
cancellationToken.cancelled = true
|
||||
console.log("Cleaning up URL processing")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of display segments from response text and URL matches
|
||||
* @param responseText - The full response text
|
||||
* @param urlMatches - Array of URL matches with their positions and types
|
||||
* @returns Array of display segments describing how to render the content
|
||||
*/
|
||||
export const buildDisplaySegments = (responseText: string, urlMatches: UrlMatch[]): DisplaySegment[] => {
|
||||
const segments: DisplaySegment[] = []
|
||||
let lastIndex = 0
|
||||
let segmentIndex = 0
|
||||
|
||||
// Handle case with no URLs
|
||||
if (urlMatches.length === 0) {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
content: responseText,
|
||||
key: "segment-0",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Process each URL match
|
||||
for (let i = 0; i < urlMatches.length; i++) {
|
||||
const match = urlMatches[i]
|
||||
const { url, fullMatch, index } = match
|
||||
|
||||
// Add text segment before this URL
|
||||
if (index > lastIndex) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: responseText.substring(lastIndex, index),
|
||||
key: `segment-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Add the URL text itself
|
||||
segments.push({
|
||||
type: "url",
|
||||
content: fullMatch,
|
||||
key: `url-${segmentIndex++}`,
|
||||
})
|
||||
|
||||
// Add embedded content after the URL
|
||||
if (match.isImage) {
|
||||
segments.push({
|
||||
type: "image",
|
||||
content: url,
|
||||
url: formatUrlForOpening(url),
|
||||
key: `embed-image-${url}-${segmentIndex++}`,
|
||||
})
|
||||
} else if (match.isProcessed && !isLocalhostUrl(url)) {
|
||||
segments.push({
|
||||
type: "link",
|
||||
content: url,
|
||||
url: formatUrlForOpening(url),
|
||||
key: `embed-${url}-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Update lastIndex for next segment
|
||||
lastIndex = index + fullMatch.length
|
||||
}
|
||||
|
||||
// Add any remaining text after the last URL
|
||||
if (lastIndex < responseText.length) {
|
||||
segments.push({
|
||||
type: "text",
|
||||
content: responseText.substring(lastIndex),
|
||||
key: `segment-${segmentIndex++}`,
|
||||
})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ const featuredModels = [
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "moonshotai/kimi-k2",
|
||||
description: "Latest open source model, trained for agentic tool calling.",
|
||||
id: "google/gemini-2.5-pro",
|
||||
description: "Large 1M context window, great value",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
id: "x-ai/grok-4",
|
||||
description: "Latest flagship model from xAI",
|
||||
label: "Fast & Cheap",
|
||||
id: "moonshotai/kimi-k2",
|
||||
description: "Open source model topping coding benchmarks",
|
||||
label: "New",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -14,13 +14,11 @@ import { TerminalProfile } from "@shared/proto/state"
|
||||
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS, BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { DEFAULT_PLATFORM, ExtensionMessage, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { DEFAULT_PLATFORM, ExtensionState } from "@shared/ExtensionMessage"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import {
|
||||
ApiConfiguration,
|
||||
ModelInfo,
|
||||
openRouterDefaultModelId,
|
||||
openRouterDefaultModelInfo,
|
||||
@@ -31,6 +29,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
|
||||
@@ -54,19 +53,8 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
showAnnouncement: boolean
|
||||
|
||||
// Setters
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setTelemetrySetting: (value: TelemetrySetting) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setShouldShowAnnouncement: (value: boolean) => void
|
||||
setPlanActSeparateModelsSetting: (value: boolean) => void
|
||||
setEnableCheckpointsSetting: (value: boolean) => void
|
||||
setMcpMarketplaceEnabled: (value: boolean) => void
|
||||
setMcpRichDisplayEnabled: (value: boolean) => void
|
||||
setMcpResponsesCollapsed: (value: boolean) => void
|
||||
setShellIntegrationTimeout: (value: number) => void
|
||||
setTerminalReuseEnabled: (value: boolean) => void
|
||||
setTerminalOutputLineLimit: (value: number) => void
|
||||
setDefaultTerminalProfile: (value: string) => void
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
@@ -78,8 +66,6 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
|
||||
setTotalTasksSize: (value: number | null) => void
|
||||
setAvailableTerminalProfiles: (profiles: TerminalProfile[]) => void // Setter for profiles
|
||||
setBrowserSettings: (value: BrowserSettings) => void
|
||||
|
||||
// Refresh functions
|
||||
refreshOpenRouterModels: () => void
|
||||
@@ -190,7 +176,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
distinctId: "",
|
||||
planActSeparateModelsSetting: true,
|
||||
enableCheckpointsSetting: true,
|
||||
mcpRichDisplayEnabled: true,
|
||||
mcpDisplayMode: DEFAULT_MCP_DISPLAY_MODE,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: {},
|
||||
@@ -676,72 +662,15 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
hideHistory,
|
||||
hideAccount,
|
||||
hideAnnouncement,
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value,
|
||||
})),
|
||||
setTelemetrySetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
telemetrySetting: value,
|
||||
})),
|
||||
setPlanActSeparateModelsSetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
planActSeparateModelsSetting: value,
|
||||
})),
|
||||
setEnableCheckpointsSetting: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
enableCheckpointsSetting: value,
|
||||
})),
|
||||
setMcpMarketplaceEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpMarketplaceEnabled: value,
|
||||
})),
|
||||
setMcpRichDisplayEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpRichDisplayEnabled: value,
|
||||
})),
|
||||
setMcpResponsesCollapsed: (value) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
mcpResponsesCollapsed: value,
|
||||
}))
|
||||
},
|
||||
setShowAnnouncement,
|
||||
setShouldShowAnnouncement: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shouldShowAnnouncement: value,
|
||||
})),
|
||||
setShellIntegrationTimeout: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
shellIntegrationTimeout: value,
|
||||
})),
|
||||
setTerminalReuseEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalReuseEnabled: value,
|
||||
})),
|
||||
setTerminalOutputLineLimit: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalOutputLineLimit: value,
|
||||
})),
|
||||
setDefaultTerminalProfile: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
defaultTerminalProfile: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setAvailableTerminalProfiles,
|
||||
setShowMcp,
|
||||
closeMcpView,
|
||||
setChatSettings: async (value) => {
|
||||
@@ -768,7 +697,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: state.mcpRichDisplayEnabled,
|
||||
mcpDisplayMode: state.mcpDisplayMode,
|
||||
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
|
||||
}),
|
||||
)
|
||||
@@ -811,11 +740,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
refreshOpenRouterModels,
|
||||
onRelinquishControl,
|
||||
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
|
||||
setBrowserSettings: (value: BrowserSettings) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
browserSettings: value,
|
||||
})),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user