mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-27 01:21:24 +00:00
* feat: optimize memory usage for image handling in webview - Replace base64 image data with webview URIs to reduce memory footprint - Add proper resource roots to webview for workspace file access - Implement convertToWebviewUri method for safe file-to-URI conversion - Update ImageViewer to handle both webview URIs and file paths separately - Add image message type for proper image rendering in chat - Improve error handling and display for failed image loads - Add comprehensive tests for ImageViewer component - Format display paths as relative for better readability This change significantly reduces memory usage by avoiding base64 encoding of images and instead using VSCode's webview URI system for direct file access. Images are now loaded on-demand from disk rather than being held in memory as base64 strings. * fix: address PR review comments - Use safeJsonParse instead of JSON.parse in ChatRow.tsx - Add type definition for parsed image info - Add more specific error types in ClineProvider.ts - Add comprehensive JSDoc comments to ImageBlock.tsx - Improve error handling and type safety * fix: address MrUbens' review comments - Remove hardcoded 'rc1' pattern in formatDisplayPath, use generic workspace detection - Internationalize 'No image data' text using i18n system * chore: remove useless comment * chore(i18n): add image.noData to all locales to fix translation check * test: update ImageViewer.spec to align with i18n key and flexible path formatting
1314 lines
40 KiB
TypeScript
1314 lines
40 KiB
TypeScript
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
import { useSize } from "react-use"
|
|
import { useTranslation, Trans } from "react-i18next"
|
|
import deepEqual from "fast-deep-equal"
|
|
import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
|
|
|
import type { ClineMessage, FollowUpData, SuggestionItem } from "@roo-code/types"
|
|
|
|
import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
|
|
import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
|
|
import { safeJsonParse } from "@roo/safeJsonParse"
|
|
|
|
import { useCopyToClipboard } from "@src/utils/clipboard"
|
|
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|
import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
|
|
import { vscode } from "@src/utils/vscode"
|
|
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
|
|
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
|
|
import { Button } from "@src/components/ui"
|
|
|
|
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
|
|
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
|
|
import CodeAccordian from "../common/CodeAccordian"
|
|
import CodeBlock from "../common/CodeBlock"
|
|
import MarkdownBlock from "../common/MarkdownBlock"
|
|
import { ReasoningBlock } from "./ReasoningBlock"
|
|
import Thumbnails from "../common/Thumbnails"
|
|
import ImageBlock from "../common/ImageBlock"
|
|
|
|
import McpResourceRow from "../mcp/McpResourceRow"
|
|
|
|
import { Mention } from "./Mention"
|
|
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
|
|
import { FollowUpSuggest } from "./FollowUpSuggest"
|
|
import { BatchFilePermission } from "./BatchFilePermission"
|
|
import { BatchDiffApproval } from "./BatchDiffApproval"
|
|
import { ProgressIndicator } from "./ProgressIndicator"
|
|
import { Markdown } from "./Markdown"
|
|
import { CommandExecution } from "./CommandExecution"
|
|
import { CommandExecutionError } from "./CommandExecutionError"
|
|
import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning"
|
|
import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
|
|
import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay"
|
|
import { McpExecution } from "./McpExecution"
|
|
|
|
interface ChatRowProps {
|
|
message: ClineMessage
|
|
lastModifiedMessage?: ClineMessage
|
|
isExpanded: boolean
|
|
isLast: boolean
|
|
isStreaming: boolean
|
|
onToggleExpand: (ts: number) => void
|
|
onHeightChange: (isTaller: boolean) => void
|
|
onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void
|
|
onBatchFileResponse?: (response: { [key: string]: boolean }) => void
|
|
onFollowUpUnmount?: () => void
|
|
isFollowUpAnswered?: boolean
|
|
editable?: boolean
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
|
|
|
const ChatRow = memo(
|
|
(props: ChatRowProps) => {
|
|
const { isLast, onHeightChange, message } = props
|
|
// Store the previous height to compare with the current height
|
|
// This allows us to detect changes without causing re-renders
|
|
const prevHeightRef = useRef(0)
|
|
|
|
const [chatrow, { height }] = useSize(
|
|
<div className="px-[15px] py-[10px] pr-[6px]">
|
|
<ChatRowContent {...props} />
|
|
</div>,
|
|
)
|
|
|
|
useEffect(() => {
|
|
// used for partials, command output, etc.
|
|
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
|
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
|
// height starts off at Infinity
|
|
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
|
if (!isInitialRender) {
|
|
onHeightChange(height > prevHeightRef.current)
|
|
}
|
|
prevHeightRef.current = height
|
|
}
|
|
}, [height, isLast, onHeightChange, message])
|
|
|
|
// we cannot return null as virtuoso does not support it, so we use a separate visibleMessages array to filter out messages that should not be rendered
|
|
return chatrow
|
|
},
|
|
// memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
|
|
deepEqual,
|
|
)
|
|
|
|
export default ChatRow
|
|
|
|
export const ChatRowContent = ({
|
|
message,
|
|
lastModifiedMessage,
|
|
isExpanded,
|
|
isLast,
|
|
isStreaming,
|
|
onToggleExpand,
|
|
onSuggestionClick,
|
|
onFollowUpUnmount,
|
|
onBatchFileResponse,
|
|
isFollowUpAnswered,
|
|
editable,
|
|
}: ChatRowContentProps) => {
|
|
const { t } = useTranslation()
|
|
|
|
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
|
|
|
|
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
|
|
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
|
|
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
|
|
|
const { copyWithFeedback } = useCopyToClipboard()
|
|
|
|
// Memoized callback to prevent re-renders caused by inline arrow functions.
|
|
const handleToggleExpand = useCallback(() => {
|
|
onToggleExpand(message.ts)
|
|
}, [onToggleExpand, message.ts])
|
|
|
|
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
|
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
|
|
const info = safeJsonParse<ClineApiReqInfo>(message.text)
|
|
return [info?.cost, info?.cancelReason, info?.streamingFailedMessage]
|
|
}
|
|
|
|
return [undefined, undefined, undefined]
|
|
}, [message.text, message.say])
|
|
|
|
// When resuming task, last wont be api_req_failed but a resume_task
|
|
// message, so api_req_started will show loading spinner. That's why we just
|
|
// remove the last api_req_started that failed without streaming anything.
|
|
const apiRequestFailedMessage =
|
|
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
|
|
? lastModifiedMessage?.text
|
|
: undefined
|
|
|
|
const isCommandExecuting =
|
|
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
|
|
|
|
const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
|
|
|
|
const type = message.type === "ask" ? message.ask : message.say
|
|
|
|
const normalColor = "var(--vscode-foreground)"
|
|
const errorColor = "var(--vscode-errorForeground)"
|
|
const successColor = "var(--vscode-charts-green)"
|
|
const cancelledColor = "var(--vscode-descriptionForeground)"
|
|
|
|
const [icon, title] = useMemo(() => {
|
|
switch (type) {
|
|
case "error":
|
|
return [
|
|
<span
|
|
className="codicon codicon-error"
|
|
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
|
<span style={{ color: errorColor, fontWeight: "bold" }}>{t("chat:error")}</span>,
|
|
]
|
|
case "mistake_limit_reached":
|
|
return [
|
|
<span
|
|
className="codicon codicon-error"
|
|
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
|
<span style={{ color: errorColor, fontWeight: "bold" }}>{t("chat:troubleMessage")}</span>,
|
|
]
|
|
case "command":
|
|
return [
|
|
isCommandExecuting ? (
|
|
<ProgressIndicator />
|
|
) : (
|
|
<span
|
|
className="codicon codicon-terminal"
|
|
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
|
),
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:runCommand.title")}:</span>,
|
|
]
|
|
case "use_mcp_server":
|
|
const mcpServerUse = safeJsonParse<ClineAskUseMcpServer>(message.text)
|
|
if (mcpServerUse === undefined) {
|
|
return [null, null]
|
|
}
|
|
return [
|
|
isMcpServerResponding ? (
|
|
<ProgressIndicator />
|
|
) : (
|
|
<span
|
|
className="codicon codicon-server"
|
|
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
|
),
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
|
{mcpServerUse.type === "use_mcp_tool"
|
|
? t("chat:mcp.wantsToUseTool", { serverName: mcpServerUse.serverName })
|
|
: t("chat:mcp.wantsToAccessResource", { serverName: mcpServerUse.serverName })}
|
|
</span>,
|
|
]
|
|
case "completion_result":
|
|
return [
|
|
<span
|
|
className="codicon codicon-check"
|
|
style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
|
|
<span style={{ color: successColor, fontWeight: "bold" }}>{t("chat:taskCompleted")}</span>,
|
|
]
|
|
case "api_req_retry_delayed":
|
|
return []
|
|
case "api_req_started":
|
|
const getIconSpan = (iconName: string, color: string) => (
|
|
<div
|
|
style={{
|
|
width: 16,
|
|
height: 16,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}>
|
|
<span
|
|
className={`codicon codicon-${iconName}`}
|
|
style={{ color, fontSize: 16, marginBottom: "-1.5px" }}
|
|
/>
|
|
</div>
|
|
)
|
|
return [
|
|
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
|
|
apiReqCancelReason === "user_cancelled" ? (
|
|
getIconSpan("error", cancelledColor)
|
|
) : (
|
|
getIconSpan("error", errorColor)
|
|
)
|
|
) : cost !== null && cost !== undefined ? (
|
|
getIconSpan("check", successColor)
|
|
) : apiRequestFailedMessage ? (
|
|
getIconSpan("error", errorColor)
|
|
) : (
|
|
<ProgressIndicator />
|
|
),
|
|
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
|
|
apiReqCancelReason === "user_cancelled" ? (
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
|
{t("chat:apiRequest.cancelled")}
|
|
</span>
|
|
) : (
|
|
<span style={{ color: errorColor, fontWeight: "bold" }}>
|
|
{t("chat:apiRequest.streamingFailed")}
|
|
</span>
|
|
)
|
|
) : cost !== null && cost !== undefined ? (
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.title")}</span>
|
|
) : apiRequestFailedMessage ? (
|
|
<span style={{ color: errorColor, fontWeight: "bold" }}>{t("chat:apiRequest.failed")}</span>
|
|
) : (
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.streaming")}</span>
|
|
),
|
|
]
|
|
case "followup":
|
|
return [
|
|
<span
|
|
className="codicon codicon-question"
|
|
style={{ color: normalColor, marginBottom: "-1.5px" }}
|
|
/>,
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:questions.hasQuestion")}</span>,
|
|
]
|
|
default:
|
|
return [null, null]
|
|
}
|
|
}, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage, t])
|
|
|
|
const headerStyle: React.CSSProperties = {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "10px",
|
|
marginBottom: "10px",
|
|
wordBreak: "break-word",
|
|
}
|
|
|
|
const pStyle: React.CSSProperties = {
|
|
margin: 0,
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-word",
|
|
overflowWrap: "anywhere",
|
|
}
|
|
|
|
const tool = useMemo(
|
|
() => (message.ask === "tool" ? safeJsonParse<ClineSayTool>(message.text) : null),
|
|
[message.ask, message.text],
|
|
)
|
|
|
|
const followUpData = useMemo(() => {
|
|
if (message.type === "ask" && message.ask === "followup" && !message.partial) {
|
|
return safeJsonParse<FollowUpData>(message.text)
|
|
}
|
|
return null
|
|
}, [message.type, message.ask, message.partial, message.text])
|
|
|
|
if (tool) {
|
|
const toolIcon = (name: string) => (
|
|
<span
|
|
className={`codicon codicon-${name}`}
|
|
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
|
|
)
|
|
|
|
switch (tool.tool) {
|
|
case "editedExistingFile":
|
|
case "appliedDiff":
|
|
// Check if this is a batch diff request
|
|
if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("diff")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{t("chat:fileOperations.wantsToApplyBatchChanges")}
|
|
</span>
|
|
</div>
|
|
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Regular single file diff
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{tool.isProtected ? (
|
|
<span
|
|
className="codicon codicon-lock"
|
|
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
|
|
/>
|
|
) : (
|
|
toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")
|
|
)}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{tool.isProtected
|
|
? t("chat:fileOperations.wantsToEditProtected")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
|
|
: t("chat:fileOperations.wantsToEdit")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content ?? tool.diff}
|
|
language="diff"
|
|
progressStatus={message.progressStatus}
|
|
isLoading={message.partial}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "insertContent":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{tool.isProtected ? (
|
|
<span
|
|
className="codicon codicon-lock"
|
|
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
|
|
/>
|
|
) : (
|
|
toolIcon("insert")
|
|
)}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{tool.isProtected
|
|
? t("chat:fileOperations.wantsToEditProtected")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
|
|
: tool.lineNumber === 0
|
|
? t("chat:fileOperations.wantsToInsertAtEnd")
|
|
: t("chat:fileOperations.wantsToInsertWithLineNumber", {
|
|
lineNumber: tool.lineNumber,
|
|
})}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.diff}
|
|
language="diff"
|
|
progressStatus={message.progressStatus}
|
|
isLoading={message.partial}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "searchAndReplace":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{tool.isProtected ? (
|
|
<span
|
|
className="codicon codicon-lock"
|
|
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
|
|
/>
|
|
) : (
|
|
toolIcon("replace")
|
|
)}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{tool.isProtected && message.type === "ask"
|
|
? t("chat:fileOperations.wantsToEditProtected")
|
|
: message.type === "ask"
|
|
? t("chat:fileOperations.wantsToSearchReplace")
|
|
: t("chat:fileOperations.didSearchReplace")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.diff}
|
|
language="diff"
|
|
progressStatus={message.progressStatus}
|
|
isLoading={message.partial}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "codebaseSearch": {
|
|
return (
|
|
<div style={headerStyle}>
|
|
{toolIcon("search")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{tool.path ? (
|
|
<Trans
|
|
i18nKey="chat:codebaseSearch.wantsToSearchWithPath"
|
|
components={{ code: <code></code> }}
|
|
values={{ query: tool.query, path: tool.path }}
|
|
/>
|
|
) : (
|
|
<Trans
|
|
i18nKey="chat:codebaseSearch.wantsToSearch"
|
|
components={{ code: <code></code> }}
|
|
values={{ query: tool.query }}
|
|
/>
|
|
)}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
case "updateTodoList" as any: {
|
|
const todos = (tool as any).todos || []
|
|
return (
|
|
<UpdateTodoListToolBlock
|
|
todos={todos}
|
|
content={(tool as any).content}
|
|
onChange={(updatedTodos) => {
|
|
if (typeof vscode !== "undefined" && vscode?.postMessage) {
|
|
vscode.postMessage({ type: "updateTodoList", payload: { todos: updatedTodos } })
|
|
}
|
|
}}
|
|
editable={editable && isLast}
|
|
/>
|
|
)
|
|
}
|
|
case "newFileCreated":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{tool.isProtected ? (
|
|
<span
|
|
className="codicon codicon-lock"
|
|
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
|
|
/>
|
|
) : (
|
|
toolIcon("new-file")
|
|
)}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{tool.isProtected
|
|
? t("chat:fileOperations.wantsToEditProtected")
|
|
: t("chat:fileOperations.wantsToCreate")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content}
|
|
language={getLanguageFromPath(tool.path || "") || "log"}
|
|
isLoading={message.partial}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
onJumpToFile={() => vscode.postMessage({ type: "openFile", text: "./" + tool.path })}
|
|
/>
|
|
</>
|
|
)
|
|
case "readFile":
|
|
// Check if this is a batch file permission request
|
|
const isBatchRequest = message.type === "ask" && tool.batchFiles && Array.isArray(tool.batchFiles)
|
|
|
|
if (isBatchRequest) {
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("files")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{t("chat:fileOperations.wantsToReadMultiple")}
|
|
</span>
|
|
</div>
|
|
<BatchFilePermission
|
|
files={tool.batchFiles || []}
|
|
onPermissionResponse={(response) => {
|
|
onBatchFileResponse?.(response)
|
|
}}
|
|
ts={message?.ts}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Regular single file read request
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("file-code")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask"
|
|
? tool.isOutsideWorkspace
|
|
? t("chat:fileOperations.wantsToReadOutsideWorkspace")
|
|
: tool.additionalFileCount && tool.additionalFileCount > 0
|
|
? t("chat:fileOperations.wantsToReadAndXMore", {
|
|
count: tool.additionalFileCount,
|
|
})
|
|
: t("chat:fileOperations.wantsToRead")
|
|
: t("chat:fileOperations.didRead")}
|
|
</span>
|
|
</div>
|
|
<ToolUseBlock>
|
|
<ToolUseBlockHeader
|
|
onClick={() => vscode.postMessage({ type: "openFile", text: tool.content })}>
|
|
{tool.path?.startsWith(".") && <span>.</span>}
|
|
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
|
|
{removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
|
|
{tool.reason}
|
|
</span>
|
|
<div style={{ flexGrow: 1 }}></div>
|
|
<span
|
|
className={`codicon codicon-link-external`}
|
|
style={{ fontSize: 13.5, margin: "1px 0" }}
|
|
/>
|
|
</ToolUseBlockHeader>
|
|
</ToolUseBlock>
|
|
</>
|
|
)
|
|
case "fetchInstructions":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("file-code")}
|
|
<span style={{ fontWeight: "bold" }}>{t("chat:instructions.wantsToFetch")}</span>
|
|
</div>
|
|
<CodeAccordian
|
|
code={tool.content}
|
|
language="markdown"
|
|
isLoading={message.partial}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "listFilesTopLevel":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("folder-opened")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask"
|
|
? tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace")
|
|
: t("chat:directoryOperations.wantsToViewTopLevel")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.didViewTopLevelOutsideWorkspace")
|
|
: t("chat:directoryOperations.didViewTopLevel")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content}
|
|
language="shell-session"
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "listFilesRecursive":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("folder-opened")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask"
|
|
? tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace")
|
|
: t("chat:directoryOperations.wantsToViewRecursive")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.didViewRecursiveOutsideWorkspace")
|
|
: t("chat:directoryOperations.didViewRecursive")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content}
|
|
language="shellsession"
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "listCodeDefinitionNames":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("file-code")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask"
|
|
? tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.wantsToViewDefinitionsOutsideWorkspace")
|
|
: t("chat:directoryOperations.wantsToViewDefinitions")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:directoryOperations.didViewDefinitionsOutsideWorkspace")
|
|
: t("chat:directoryOperations.didViewDefinitions")}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content}
|
|
language="markdown"
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "searchFiles":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("search")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask" ? (
|
|
<Trans
|
|
i18nKey={
|
|
tool.isOutsideWorkspace
|
|
? "chat:directoryOperations.wantsToSearchOutsideWorkspace"
|
|
: "chat:directoryOperations.wantsToSearch"
|
|
}
|
|
components={{ code: <code>{tool.regex}</code> }}
|
|
values={{ regex: tool.regex }}
|
|
/>
|
|
) : (
|
|
<Trans
|
|
i18nKey={
|
|
tool.isOutsideWorkspace
|
|
? "chat:directoryOperations.didSearchOutsideWorkspace"
|
|
: "chat:directoryOperations.didSearch"
|
|
}
|
|
components={{ code: <code>{tool.regex}</code> }}
|
|
values={{ regex: tool.regex }}
|
|
/>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<CodeAccordian
|
|
path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
|
|
code={tool.content}
|
|
language="shellsession"
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</>
|
|
)
|
|
case "switchMode":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("symbol-enum")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask" ? (
|
|
<>
|
|
{tool.reason ? (
|
|
<Trans
|
|
i18nKey="chat:modes.wantsToSwitchWithReason"
|
|
components={{ code: <code>{tool.mode}</code> }}
|
|
values={{ mode: tool.mode, reason: tool.reason }}
|
|
/>
|
|
) : (
|
|
<Trans
|
|
i18nKey="chat:modes.wantsToSwitch"
|
|
components={{ code: <code>{tool.mode}</code> }}
|
|
values={{ mode: tool.mode }}
|
|
/>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{tool.reason ? (
|
|
<Trans
|
|
i18nKey="chat:modes.didSwitchWithReason"
|
|
components={{ code: <code>{tool.mode}</code> }}
|
|
values={{ mode: tool.mode, reason: tool.reason }}
|
|
/>
|
|
) : (
|
|
<Trans
|
|
i18nKey="chat:modes.didSwitch"
|
|
components={{ code: <code>{tool.mode}</code> }}
|
|
values={{ mode: tool.mode }}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</>
|
|
)
|
|
case "newTask":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("tasklist")}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
<Trans
|
|
i18nKey="chat:subtasks.wantsToCreate"
|
|
components={{ code: <code>{tool.mode}</code> }}
|
|
values={{ mode: tool.mode }}
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: "4px",
|
|
backgroundColor: "var(--vscode-badge-background)",
|
|
border: "1px solid var(--vscode-badge-background)",
|
|
borderRadius: "4px 4px 0 0",
|
|
overflow: "hidden",
|
|
marginBottom: "2px",
|
|
}}>
|
|
<div
|
|
style={{
|
|
padding: "9px 10px 9px 14px",
|
|
backgroundColor: "var(--vscode-badge-background)",
|
|
borderBottom: "1px solid var(--vscode-editorGroup-border)",
|
|
fontWeight: "bold",
|
|
fontSize: "var(--vscode-font-size)",
|
|
color: "var(--vscode-badge-foreground)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
}}>
|
|
<span className="codicon codicon-arrow-right"></span>
|
|
{t("chat:subtasks.newTaskContent")}
|
|
</div>
|
|
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
|
|
<MarkdownBlock markdown={tool.content} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
case "finishTask":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{toolIcon("check-all")}
|
|
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.wantsToFinish")}</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: "4px",
|
|
backgroundColor: "var(--vscode-editor-background)",
|
|
border: "1px solid var(--vscode-badge-background)",
|
|
borderRadius: "4px",
|
|
overflow: "hidden",
|
|
marginBottom: "8px",
|
|
}}>
|
|
<div
|
|
style={{
|
|
padding: "9px 10px 9px 14px",
|
|
backgroundColor: "var(--vscode-badge-background)",
|
|
borderBottom: "1px solid var(--vscode-editorGroup-border)",
|
|
fontWeight: "bold",
|
|
fontSize: "var(--vscode-font-size)",
|
|
color: "var(--vscode-badge-foreground)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
}}>
|
|
<span className="codicon codicon-check"></span>
|
|
{t("chat:subtasks.completionContent")}
|
|
</div>
|
|
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
|
|
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
case "generateImage":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{tool.isProtected ? (
|
|
<span
|
|
className="codicon codicon-lock"
|
|
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
|
|
/>
|
|
) : (
|
|
toolIcon("file-media")
|
|
)}
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{message.type === "ask"
|
|
? tool.isProtected
|
|
? t("chat:fileOperations.wantsToGenerateImageProtected")
|
|
: tool.isOutsideWorkspace
|
|
? t("chat:fileOperations.wantsToGenerateImageOutsideWorkspace")
|
|
: t("chat:fileOperations.wantsToGenerateImage")
|
|
: t("chat:fileOperations.didGenerateImage")}
|
|
</span>
|
|
</div>
|
|
{message.type === "ask" && (
|
|
<CodeAccordian
|
|
path={tool.path}
|
|
code={tool.content}
|
|
language="text"
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
switch (message.type) {
|
|
case "say":
|
|
switch (message.say) {
|
|
case "diff_error":
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
marginTop: "0px",
|
|
overflow: "hidden",
|
|
marginBottom: "8px",
|
|
}}>
|
|
<div
|
|
style={{
|
|
borderBottom: isDiffErrorExpanded
|
|
? "1px solid var(--vscode-editorGroup-border)"
|
|
: "none",
|
|
fontWeight: "normal",
|
|
fontSize: "var(--vscode-font-size)",
|
|
color: "var(--vscode-editor-foreground)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={() => setIsDiffErrorExpanded(!isDiffErrorExpanded)}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "10px",
|
|
flexGrow: 1,
|
|
}}>
|
|
<span
|
|
className="codicon codicon-warning"
|
|
style={{
|
|
color: "var(--vscode-editorWarning-foreground)",
|
|
opacity: 0.8,
|
|
fontSize: 16,
|
|
marginBottom: "-1.5px",
|
|
}}></span>
|
|
<span style={{ fontWeight: "bold" }}>{t("chat:diffError.title")}</span>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<VSCodeButton
|
|
appearance="icon"
|
|
style={{
|
|
padding: "3px",
|
|
height: "24px",
|
|
marginRight: "4px",
|
|
color: "var(--vscode-editor-foreground)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
background: "transparent",
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
|
|
// Call copyWithFeedback and handle the Promise
|
|
copyWithFeedback(message.text || "").then((success) => {
|
|
if (success) {
|
|
// Show checkmark
|
|
setShowCopySuccess(true)
|
|
|
|
// Reset after a brief delay
|
|
setTimeout(() => {
|
|
setShowCopySuccess(false)
|
|
}, 1000)
|
|
}
|
|
})
|
|
}}>
|
|
<span
|
|
className={`codicon codicon-${showCopySuccess ? "check" : "copy"}`}></span>
|
|
</VSCodeButton>
|
|
<span
|
|
className={`codicon codicon-chevron-${isDiffErrorExpanded ? "up" : "down"}`}></span>
|
|
</div>
|
|
</div>
|
|
{isDiffErrorExpanded && (
|
|
<div
|
|
style={{
|
|
padding: "8px",
|
|
backgroundColor: "var(--vscode-editor-background)",
|
|
borderTop: "none",
|
|
}}>
|
|
<CodeBlock source={message.text || ""} language="xml" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
case "subtask_result":
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
marginTop: "0px",
|
|
backgroundColor: "var(--vscode-badge-background)",
|
|
border: "1px solid var(--vscode-badge-background)",
|
|
borderRadius: "0 0 4px 4px",
|
|
overflow: "hidden",
|
|
marginBottom: "8px",
|
|
}}>
|
|
<div
|
|
style={{
|
|
padding: "9px 10px 9px 14px",
|
|
backgroundColor: "var(--vscode-badge-background)",
|
|
borderBottom: "1px solid var(--vscode-editorGroup-border)",
|
|
fontWeight: "bold",
|
|
fontSize: "var(--vscode-font-size)",
|
|
color: "var(--vscode-badge-foreground)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
}}>
|
|
<span className="codicon codicon-arrow-left"></span>
|
|
{t("chat:subtasks.resultContent")}
|
|
</div>
|
|
<div
|
|
style={{
|
|
padding: "12px 16px",
|
|
backgroundColor: "var(--vscode-editor-background)",
|
|
}}>
|
|
<MarkdownBlock markdown={message.text} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
case "reasoning":
|
|
return (
|
|
<ReasoningBlock
|
|
content={message.text || ""}
|
|
elapsed={isLast && isStreaming ? Date.now() - message.ts : undefined}
|
|
isCollapsed={reasoningCollapsed}
|
|
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
|
|
/>
|
|
)
|
|
case "api_req_started":
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
...headerStyle,
|
|
marginBottom:
|
|
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
|
|
apiReqStreamingFailedMessage
|
|
? 10
|
|
: 0,
|
|
justifyContent: "space-between",
|
|
cursor: "pointer",
|
|
userSelect: "none",
|
|
WebkitUserSelect: "none",
|
|
MozUserSelect: "none",
|
|
msUserSelect: "none",
|
|
}}
|
|
onClick={handleToggleExpand}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
|
{icon}
|
|
{title}
|
|
<VSCodeBadge
|
|
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
|
|
${Number(cost || 0)?.toFixed(4)}
|
|
</VSCodeBadge>
|
|
</div>
|
|
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
|
</div>
|
|
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
|
|
apiReqStreamingFailedMessage) && (
|
|
<>
|
|
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
|
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
|
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
|
|
<>
|
|
<br />
|
|
<br />
|
|
{t("chat:powershell.issues")}{" "}
|
|
<a
|
|
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
|
|
style={{ color: "inherit", textDecoration: "underline" }}>
|
|
troubleshooting guide
|
|
</a>
|
|
.
|
|
</>
|
|
)}
|
|
</p>
|
|
</>
|
|
)}
|
|
|
|
{isExpanded && (
|
|
<div style={{ marginTop: "10px" }}>
|
|
<CodeAccordian
|
|
code={safeJsonParse<any>(message.text)?.request}
|
|
language="markdown"
|
|
isExpanded={true}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
case "api_req_finished":
|
|
return null // we should never see this message type
|
|
case "text":
|
|
return (
|
|
<div>
|
|
<Markdown markdown={message.text} partial={message.partial} />
|
|
{message.images && message.images.length > 0 && (
|
|
<div style={{ marginTop: "10px" }}>
|
|
{message.images.map((image, index) => (
|
|
<ImageBlock key={index} imageData={image} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
case "user_feedback":
|
|
return (
|
|
<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
|
|
<div className="flex justify-between">
|
|
<div className="flex-grow px-2 py-1 wrap-anywhere">
|
|
<Mention text={message.text} withShadow />
|
|
</div>
|
|
<div className="flex">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="shrink-0"
|
|
disabled={isStreaming}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
vscode.postMessage({ type: "deleteMessage", value: message.ts })
|
|
}}>
|
|
<span className="codicon codicon-trash" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{message.images && message.images.length > 0 && (
|
|
<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
|
|
)}
|
|
</div>
|
|
)
|
|
case "user_feedback_diff":
|
|
const tool = safeJsonParse<ClineSayTool>(message.text)
|
|
return (
|
|
<div style={{ marginTop: -10, width: "100%" }}>
|
|
<CodeAccordian
|
|
code={tool?.diff}
|
|
language="diff"
|
|
isFeedback={true}
|
|
isExpanded={isExpanded}
|
|
onToggleExpand={handleToggleExpand}
|
|
/>
|
|
</div>
|
|
)
|
|
case "error":
|
|
return (
|
|
<>
|
|
{title && (
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
)}
|
|
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
</>
|
|
)
|
|
case "completion_result":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
|
<Markdown markdown={message.text} />
|
|
</div>
|
|
</>
|
|
)
|
|
case "shell_integration_warning":
|
|
return <CommandExecutionError />
|
|
case "checkpoint_saved":
|
|
return (
|
|
<CheckpointSaved
|
|
ts={message.ts!}
|
|
commitHash={message.text!}
|
|
currentHash={currentCheckpoint}
|
|
checkpoint={message.checkpoint}
|
|
/>
|
|
)
|
|
case "condense_context":
|
|
if (message.partial) {
|
|
return <CondensingContextRow />
|
|
}
|
|
return message.contextCondense ? <ContextCondenseRow {...message.contextCondense} /> : null
|
|
case "condense_context_error":
|
|
return <CondenseContextErrorRow errorText={message.text} />
|
|
case "codebase_search_result":
|
|
let parsed: {
|
|
content: {
|
|
query: string
|
|
results: Array<{
|
|
filePath: string
|
|
score: number
|
|
startLine: number
|
|
endLine: number
|
|
codeChunk: string
|
|
}>
|
|
}
|
|
} | null = null
|
|
|
|
try {
|
|
if (message.text) {
|
|
parsed = JSON.parse(message.text)
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to parse codebaseSearch content:", error)
|
|
}
|
|
|
|
if (parsed && !parsed?.content) {
|
|
console.error("Invalid codebaseSearch content structure:", parsed.content)
|
|
return <div>Error displaying search results.</div>
|
|
}
|
|
|
|
const { results = [] } = parsed?.content || {}
|
|
|
|
return <CodebaseSearchResultsDisplay results={results} />
|
|
case "user_edit_todos":
|
|
return <UpdateTodoListToolBlock userEdited onChange={() => {}} />
|
|
case "image":
|
|
// Parse the JSON to get imageUri and imagePath
|
|
const imageInfo = safeJsonParse<{ imageUri: string; imagePath: string }>(message.text || "{}")
|
|
if (!imageInfo) {
|
|
return null
|
|
}
|
|
return (
|
|
<div style={{ marginTop: "10px" }}>
|
|
<ImageBlock imageUri={imageInfo.imageUri} imagePath={imageInfo.imagePath} />
|
|
</div>
|
|
)
|
|
default:
|
|
return (
|
|
<>
|
|
{title && (
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
)}
|
|
<div style={{ paddingTop: 10 }}>
|
|
<Markdown markdown={message.text} partial={message.partial} />
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
case "ask":
|
|
switch (message.ask) {
|
|
case "mistake_limit_reached":
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
</>
|
|
)
|
|
case "command":
|
|
return (
|
|
<CommandExecution
|
|
executionId={message.ts.toString()}
|
|
text={message.text}
|
|
icon={icon}
|
|
title={title}
|
|
/>
|
|
)
|
|
case "use_mcp_server":
|
|
// Parse the message text to get the MCP server request
|
|
const messageJson = safeJsonParse<any>(message.text, {})
|
|
|
|
// Extract the response field if it exists
|
|
const { response, ...mcpServerRequest } = messageJson
|
|
|
|
// Create the useMcpServer object with the response field
|
|
const useMcpServer: ClineAskUseMcpServer = {
|
|
...mcpServerRequest,
|
|
response,
|
|
}
|
|
|
|
if (!useMcpServer) {
|
|
return null
|
|
}
|
|
|
|
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
|
|
|
|
return (
|
|
<>
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2 mt-2">
|
|
{useMcpServer.type === "access_mcp_resource" && (
|
|
<McpResourceRow
|
|
item={{
|
|
// Use the matched resource/template details, with fallbacks
|
|
...(findMatchingResourceOrTemplate(
|
|
useMcpServer.uri || "",
|
|
server?.resources,
|
|
server?.resourceTemplates,
|
|
) || {
|
|
name: "",
|
|
mimeType: "",
|
|
description: "",
|
|
}),
|
|
// Always use the actual URI from the request
|
|
uri: useMcpServer.uri || "",
|
|
}}
|
|
/>
|
|
)}
|
|
{useMcpServer.type === "use_mcp_tool" && (
|
|
<McpExecution
|
|
executionId={message.ts.toString()}
|
|
text={useMcpServer.arguments !== "{}" ? useMcpServer.arguments : undefined}
|
|
serverName={useMcpServer.serverName}
|
|
toolName={useMcpServer.toolName}
|
|
isArguments={true}
|
|
server={server}
|
|
useMcpServer={useMcpServer}
|
|
alwaysAllowMcp={alwaysAllowMcp}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
case "completion_result":
|
|
if (message.text) {
|
|
return (
|
|
<div>
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
|
<Markdown markdown={message.text} partial={message.partial} />
|
|
</div>
|
|
</div>
|
|
)
|
|
} else {
|
|
return null // Don't render anything when we get a completion_result ask without text
|
|
}
|
|
case "followup":
|
|
return (
|
|
<>
|
|
{title && (
|
|
<div style={headerStyle}>
|
|
{icon}
|
|
{title}
|
|
</div>
|
|
)}
|
|
<div style={{ paddingTop: 10, paddingBottom: 15 }}>
|
|
<Markdown
|
|
markdown={message.partial === true ? message?.text : followUpData?.question}
|
|
/>
|
|
</div>
|
|
<FollowUpSuggest
|
|
suggestions={followUpData?.suggest}
|
|
onSuggestionClick={onSuggestionClick}
|
|
ts={message?.ts}
|
|
onCancelAutoApproval={onFollowUpUnmount}
|
|
isAnswered={isFollowUpAnswered}
|
|
/>
|
|
</>
|
|
)
|
|
case "auto_approval_max_req_reached": {
|
|
return <AutoApprovedRequestLimitWarning message={message} />
|
|
}
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
}
|