Revert "Tailwind migration" (#3321)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Matt Rubens 2025-05-08 10:38:29 -04:00 committed by GitHub
parent ba465b834e
commit 6d8f903b04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1679 additions and 840 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Revert "Tailwind migration"

View file

@ -1,94 +1,113 @@
import { useState, memo } from "react"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Trans } from "react-i18next"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@src/components/ui"
interface AnnouncementProps {
hideAnnouncement: () => void
}
/**
* You must update the `latestAnnouncementId` in ClineProvider for new
* announcements to show to users. This new id will be compared with what's in
* state for the 'last announcement shown', and if it's different then the
* announcement will render. As soon as an announcement is shown, the id will be
* updated in state. This ensures that announcements are not shown more than
* once, even if the user doesn't close it themselves.
*/
/*
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
*/
const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
const { t } = useAppTranslation()
const [open, setOpen] = useState(true)
const discordLink = (
<VSCodeLink
href="https://discord.gg/roocode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://discord.gg/roocode" } },
"*",
)
}}>
Discord
</VSCodeLink>
)
const redditLink = (
<VSCodeLink
href="https://reddit.com/r/RooCode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://reddit.com/r/RooCode" } },
"*",
)
}}>
Reddit
</VSCodeLink>
)
return (
<Dialog
open={open}
onOpenChange={(open) => {
setOpen(open)
<div className="flex flex-col justify-center absolute top-0 bottom-0 left-0 right-0 z-50 p-10 bg-black/50">
<div
style={{
backgroundColor: "var(--vscode-editor-background)",
borderRadius: "3px",
padding: "12px 16px",
margin: "5px 15px 5px 15px",
position: "relative",
flexShrink: 0,
}}>
<VSCodeButton
appearance="icon"
onClick={hideAnnouncement}
title={t("chat:announcement.hideButton")}
style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h2 style={{ margin: "0 0 8px" }}>{t("chat:announcement.title")}</h2>
if (!open) {
hideAnnouncement()
}
}}>
<DialogContent className="max-w-96">
<DialogHeader>
<DialogTitle>{t("chat:announcement.title")}</DialogTitle>
<DialogDescription>{t("chat:announcement.description")}</DialogDescription>
</DialogHeader>
<div>
<h3>{t("chat:announcement.whatsNew")}</h3>
<ul className="space-y-2">
<li>
{" "}
<Trans i18nKey="chat:announcement.feature1" components={{ bold: <b />, code: <code /> }} />
</li>
<li>
{" "}
<Trans i18nKey="chat:announcement.feature2" components={{ bold: <b />, code: <code /> }} />
</li>
<li>
{" "}
<Trans i18nKey="chat:announcement.feature3" components={{ bold: <b />, code: <code /> }} />
</li>
</ul>
<p style={{ margin: "5px 0px" }}>{t("chat:announcement.description")}</p>
<h3 style={{ margin: "12px 0 5px", fontSize: "14px" }}>{t("chat:announcement.whatsNew")}</h3>
<ul style={{ margin: "5px 0" }}>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature1"
components={{
bold: <b />,
code: <code />,
}}
/>
</li>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature2"
components={{
bold: <b />,
code: <code />,
}}
/>
</li>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature3"
components={{
bold: <b />,
code: <code />,
}}
/>
</li>
</ul>
<p style={{ margin: "10px 0px 0px" }}>
<Trans
i18nKey="chat:announcement.detailsDiscussLinks"
components={{ discordLink: <DiscordLink />, redditLink: <RedditLink /> }}
components={{
discordLink: discordLink,
redditLink: redditLink,
}}
/>
</div>
</DialogContent>
</Dialog>
</p>
</div>
</div>
)
}
const DiscordLink = () => (
<VSCodeLink
href="https://discord.gg/roocode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://discord.gg/roocode" } },
"*",
)
}}>
Discord
</VSCodeLink>
)
const RedditLink = () => (
<VSCodeLink
href="https://reddit.com/r/RooCode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://reddit.com/r/RooCode" } },
"*",
)
}}>
Reddit
</VSCodeLink>
)
export default memo(Announcement)

View file

@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from "react"
import { Trans } from "react-i18next"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { cn } from "@/lib/utils"
import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useAppTranslation } from "@src/i18n/TranslationContext"
@ -119,13 +118,23 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
return (
<div
className={cn(
"px-[15px] select-none overflow-y-auto",
isExpanded && "border-t-[0.5px] border-vscode-titleBar-inactiveForeground-20",
)}
style={style}>
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
: "none",
overflowY: "auto",
...style,
}}>
<div
className={`flex items-center gap-2 cursor-pointer ${isExpanded ? "py-2" : "pt-2 pb-0"}`}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: "pointer",
}}
onClick={toggleExpanded}>
<div onClick={(e) => e.stopPropagation()}>
<VSCodeCheckbox
@ -137,22 +146,49 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
}}
/>
</div>
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="text-vscode-foreground flex-shrink-0">{t("chat:autoApprove.title")}</span>
<span className="text-vscode-descriptionForeground overflow-hidden text-ellipsis whitespace-nowrap flex-1 min-w-0">
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flex: 1,
minWidth: 0,
}}>
<span
style={{
color: "var(--vscode-foreground)",
flexShrink: 0,
}}>
{t("chat:autoApprove.title")}
</span>
<span
style={{
color: "var(--vscode-descriptionForeground)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
minWidth: 0,
}}>
{enabledActionsList || t("chat:autoApprove.none")}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} flex-shrink-0 ${
isExpanded ? "ml-0.5" : "ml-[-2px]"
}`}
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
/>
</div>
</div>
{isExpanded && (
<div className="flex flex-col gap-2">
<div className="text-vscode-descriptionForeground text-xs">
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<Trans
i18nKey="chat:autoApprove.description"
components={{

View file

@ -9,8 +9,7 @@ import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"
import CodeBlock from "../common/CodeBlock"
import { cn } from "@/lib/utils"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { ChatRowContent } from "./ChatRow"
import { ProgressIndicator } from "./ProgressIndicator"
@ -234,26 +233,53 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
: displayState.mousePosition || defaultMousePosition
const [browserSessionRow, { height: rowHeight }] = useSize(
<div className="px-[15px] py-[10px] pr-[6px] mb-[-10px]">
<div className="flex items-center gap-2.5 mb-2.5">
<div style={{ padding: "10px 6px 10px 15px", marginBottom: -10 }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "10px" }}>
{isBrowsing ? (
<ProgressIndicator />
) : (
<span className={`codicon codicon-inspect text-vscode-foreground mb-[-1.5px]`}></span>
<span
className={`codicon codicon-inspect`}
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
<>{t("chat:browser.rooWantsToUse")}</>
</span>
</div>
<div className="rounded-[3px] border border-vscode-editorGroup-border overflow-hidden mb-2.5 bg-background">
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
marginBottom: 10,
}}>
{/* URL Bar */}
<div
className={cn(
"my-[5px] mx-auto w-[calc(100%-10px)] box-border bg-vscode-input-background border border-vscode-input-border rounded p-[3px_5px] flex items-center justify-center text-xs",
// Conditional color class:
displayState.url ? "text-vscode-input-foreground" : "text-vscode-descriptionForeground",
)}>
<div className="text-ellipsis overflow-hidden whitespace-nowrap w-full text-center">
style={{
margin: "5px auto",
width: "calc(100% - 10px)",
boxSizing: "border-box", // includes padding in width calculation
backgroundColor: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "4px",
padding: "3px 5px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: displayState.url
? "var(--vscode-input-foreground)"
: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<div
style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
width: "100%",
textAlign: "center",
}}>
{displayState.url || "http"}
</div>
</div>
@ -261,15 +287,25 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
{/* Screenshot Area */}
<div
data-testid="screenshot-container"
className={cn(
"w-full relative bg-vscode-input-background",
`[padding-bottom:${aspectRatio}%]`, // Converted paddingBottom to arbitrary property
)}>
style={{
width: "100%",
paddingBottom: `${aspectRatio}%`, // height/width ratio
position: "relative",
backgroundColor: "var(--vscode-input-background)",
}}>
{displayState.screenshot ? (
<img
src={displayState.screenshot}
alt={t("chat:browser.screenshot")}
className="absolute top-0 left-0 w-full h-full object-contain cursor-pointer"
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "contain",
cursor: "pointer",
}}
onClick={() =>
vscode.postMessage({
type: "openImage",
@ -278,34 +314,47 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
}
/>
) : (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<span className="codicon codicon-globe text-[80px] text-vscode-descriptionForeground" />
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<span
className="codicon codicon-globe"
style={{ fontSize: "80px", color: "var(--vscode-descriptionForeground)" }}
/>
</div>
)}
{displayState.mousePosition && (
<BrowserCursor
className={cn(
"absolute",
// Dynamic position using arbitrary properties:
`[top:${(parseInt(mousePosition.split(",")[1]) / viewportHeight) * 100}%]`,
`[left:${(parseInt(mousePosition.split(",")[0]) / viewportWidth) * 100}%]`,
// Static transition classes:
"transition-[top,left] duration-300 ease-out",
)}
style={{
position: "absolute",
top: `${(parseInt(mousePosition.split(",")[1]) / viewportHeight) * 100}%`,
left: `${(parseInt(mousePosition.split(",")[0]) / viewportWidth) * 100}%`,
transition: "top 0.3s ease-out, left 0.3s ease-out",
}}
/>
)}
</div>
<div className="w-full">
<div style={{ width: "100%" }}>
<div
onClick={() => {
setConsoleLogsExpanded(!consoleLogsExpanded)
}}
className={`flex items-center gap-1 w-full justify-start cursor-pointer px-2 ${
consoleLogsExpanded ? "pt-[9px] pb-0" : "py-[9px]"
}`}>
style={{
display: "flex",
alignItems: "center",
gap: "4px",
width: "100%",
justifyContent: "flex-start",
cursor: "pointer",
padding: `9px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
}}>
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
<span className="text-[0.8em]">{t("chat:browser.consoleLogs")}</span>
<span style={{ fontSize: "0.8em" }}>{t("chat:browser.consoleLogs")}</span>
</div>
{consoleLogsExpanded && (
<CodeBlock source={displayState.consoleLogs || t("chat:browser.noNewLogs")} language="shell" />
@ -314,15 +363,23 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
</div>
{/* Action content with min height */}
<div className={cn(maxActionHeight > 0 && `[min-height:${maxActionHeight}px]`)}>{actionContent}</div>
<div style={{ minHeight: maxActionHeight }}>{actionContent}</div>
{/* Pagination moved to bottom */}
{pages.length > 1 && (
<div className="flex justify-between items-center py-2 mt-[15px] border-t border-vscode-editorGroup-border">
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px 0px",
marginTop: "15px",
borderTop: "1px solid var(--vscode-editorGroup-border)",
}}>
<div>
{t("chat:browser.navigation.step", { current: currentPageIndex + 1, total: pages.length })}
</div>
<div className="flex gap-1">
<div style={{ display: "flex", gap: "4px" }}>
<VSCodeButton
disabled={currentPageIndex === 0 || isBrowsing}
onClick={() => setCurrentPageIndex((i) => i - 1)}>
@ -369,7 +426,13 @@ const BrowserSessionRowContent = ({
isStreaming,
}: BrowserSessionRowContentProps) => {
const { t } = useTranslation()
const headerClassNames = "flex items-center gap-2.5 mb-2.5 break-words"
const headerStyle: React.CSSProperties = {
display: "flex",
alignItems: "center",
gap: "10px",
marginBottom: "10px",
wordBreak: "break-word",
}
switch (message.type) {
case "say":
@ -377,7 +440,7 @@ const BrowserSessionRowContent = ({
case "api_req_started":
case "text":
return (
<div className="py-2.5">
<div style={{ padding: "10px 0 10px 0" }}>
<ChatRowContent
message={message}
isExpanded={isExpanded(message.ts)}
@ -413,10 +476,16 @@ const BrowserSessionRowContent = ({
case "browser_action_launch":
return (
<>
<div className={headerClassNames}>
<span className="font-bold">{t("chat:browser.sessionStarted")}</span>
<div style={headerStyle}>
<span style={{ fontWeight: "bold" }}>{t("chat:browser.sessionStarted")}</span>
</div>
<div className="rounded-[3px] border border-vscode-editorGroup-border overflow-hidden bg-background">
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock source={message.text} language="shell" />
</div>
</>
@ -457,11 +526,26 @@ const BrowserActionBox = ({
}
}
return (
<div className="pt-2.5">
<div className="rounded-[3px] overflow-hidden border border-vscode-editorGroup-border bg-background">
<div className="flex items-center p-[9px_10px]">
<span className="whitespace-normal break-words">
<span className="font-medium">{t("chat:browser.actions.title")}</span>
<div style={{ padding: "10px 0 0 0" }}>
<div
style={{
borderRadius: 3,
backgroundColor: CODE_BLOCK_BG_COLOR,
overflow: "hidden",
border: "1px solid var(--vscode-editorGroup-border)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
padding: "9px 10px",
}}>
<span
style={{
whiteSpace: "normal",
wordBreak: "break-word",
}}>
<span style={{ fontWeight: 500 }}>{t("chat:browser.actions.title")}</span>
{getBrowserActionText(action, coordinate, text)}
</span>
</div>
@ -470,12 +554,7 @@ const BrowserActionBox = ({
)
}
interface BrowserCursorProps {
style?: React.CSSProperties
className?: string
}
const BrowserCursor: React.FC<BrowserCursorProps> = ({ style, className }) => {
const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
const { t } = useTranslation()
// (can't use svgs in vsc extensions)
const cursorBase64 =
@ -484,16 +563,13 @@ const BrowserCursor: React.FC<BrowserCursorProps> = ({ style, className }) => {
return (
<img
src={cursorBase64}
style={{
width: "17px",
height: "22px",
...style,
}}
alt={t("chat:browser.cursor")}
title={t("chat:browser.cursor")}
className={cn(
className,
"w-[17px] h-[24px]",
"translate-x-[-2px] translate-y-[-2px]", // transform
"pointer-events-none",
"z-[1000]",
)}
style={style} // Keep passed style prop for overrides
aria-label={t("chat:browser.cursor")}
/>
)
}

View file

@ -8,7 +8,6 @@ import { ClineApiReqInfo, ClineAskUseMcpServer, ClineMessage, ClineSayTool } fro
import { COMMAND_OUTPUT_STRING } from "@roo/shared/combineCommandSequences"
import { safeJsonParse } from "@roo/shared/safeJsonParse"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
@ -16,7 +15,7 @@ import { vscode } from "@src/utils/vscode"
import { Button } from "@src/components/ui"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock from "../common/CodeBlock"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
@ -119,26 +118,37 @@ export const ChatRowContent = ({
const type = message.type === "ask" ? message.ask : message.say
const [icon, title] = useMemo((): [React.ReactNode | null, React.ReactNode | null] => {
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 text-vscode-errorForeground mb-[-1.5px]" />,
<span className="font-bold text-vscode-errorForeground">{t("chat:error")}</span>,
<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 text-vscode-errorForeground mb-[-1.5px]" />, // Self-closing
<span className="font-bold text-vscode-errorForeground">{t("chat:troubleMessage")}</span>,
<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 text-vscode-foreground mb-[-1.5px]" /> // Self-closing
<span
className="codicon codicon-terminal"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span className="font-bold text-vscode-foreground">{t("chat:runCommand.title")}:</span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:runCommand.title")}:</span>,
]
case "use_mcp_server":
const mcpServerUse = safeJsonParse<ClineAskUseMcpServer>(message.text)
@ -149,9 +159,11 @@ export const ChatRowContent = ({
isMcpServerResponding ? (
<ProgressIndicator />
) : (
<span className="codicon codicon-server text-vscode-foreground mb-[-1.5px]" /> // Self-closing
<span
className="codicon codicon-server"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span className="font-bold text-vscode-foreground">
<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 })}
@ -159,59 +171,88 @@ export const ChatRowContent = ({
]
case "completion_result":
return [
<span className="codicon codicon-check text-vscode-charts-green mb-[-1.5px]" />, // Self-closing
<span className="font-bold text-vscode-charts-green">{t("chat:taskCompleted")}</span>,
<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 [null, null] // Explicitly return [null, null] for this case
return []
case "api_req_started":
const getIconSpan = (iconName: string, colorClassName: string) => (
<div className="w-4 h-4 flex items-center justify-center">
<span className={`codicon codicon-${iconName} ${colorClassName} text-[16px] mb-[-1.5px]`} />
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", "text-vscode-descriptionForeground")
getIconSpan("error", cancelledColor)
) : (
getIconSpan("error", "text-vscode-errorForeground")
getIconSpan("error", errorColor)
)
) : cost !== null && cost !== undefined ? (
getIconSpan("check", "text-vscode-charts-green")
getIconSpan("check", successColor)
) : apiRequestFailedMessage ? (
getIconSpan("error", "text-vscode-errorForeground")
getIconSpan("error", errorColor)
) : (
<ProgressIndicator />
),
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
apiReqCancelReason === "user_cancelled" ? (
<span className="font-bold text-vscode-foreground">{t("chat:apiRequest.cancelled")}</span>
<span style={{ color: normalColor, fontWeight: "bold" }}>
{t("chat:apiRequest.cancelled")}
</span>
) : (
<span className="font-bold text-vscode-errorForeground">
<span style={{ color: errorColor, fontWeight: "bold" }}>
{t("chat:apiRequest.streamingFailed")}
</span>
)
) : cost !== null && cost !== undefined ? (
<span className="font-bold text-vscode-foreground">{t("chat:apiRequest.title")}</span>
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.title")}</span>
) : apiRequestFailedMessage ? (
<span className="font-bold text-vscode-errorForeground">{t("chat:apiRequest.failed")}</span>
<span style={{ color: errorColor, fontWeight: "bold" }}>{t("chat:apiRequest.failed")}</span>
) : (
<span className="font-bold text-vscode-foreground">{t("chat:apiRequest.streaming")}</span>
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.streaming")}</span>
),
]
case "followup":
return [
<span className="codicon codicon-question text-vscode-foreground mb-[-1.5px]" />,
<span className="font-bold text-vscode-foreground">{t("chat:questions.hasQuestion")}</span>,
<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] // Explicitly return [null, null] for default case
return [null, null]
}
}, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage, t])
const headerClasses = "flex items-center gap-[10px] mb-[10px] break-words"
const pClasses = "m-0 whitespace-pre-wrap break-words overflow-anywhere"
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),
@ -227,7 +268,9 @@ export const ChatRowContent = ({
if (tool) {
const toolIcon = (name: string) => (
<span className={`codicon codicon-${name} text-vscode-foreground mb-[-1.5px]`} /> // Converted to Tailwind & self-closing
<span
className={`codicon codicon-${name}`}
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)
switch (tool.tool) {
@ -235,9 +278,9 @@ export const ChatRowContent = ({
case "appliedDiff":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: t("chat:fileOperations.wantsToEdit")}
@ -256,9 +299,9 @@ export const ChatRowContent = ({
case "insertContent":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("insert")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: tool.lineNumber === 0
@ -281,9 +324,9 @@ export const ChatRowContent = ({
case "searchAndReplace":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("replace")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:fileOperations.wantsToSearchReplace")
: t("chat:fileOperations.didSearchReplace")}
@ -302,9 +345,9 @@ export const ChatRowContent = ({
case "newFileCreated":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("new-file")}
<span className="font-bold">{t("chat:fileOperations.wantsToCreate")}</span>
<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToCreate")}</span>
</div>
<CodeAccordian
isLoading={message.partial}
@ -318,9 +361,9 @@ export const ChatRowContent = ({
case "readFile":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("file-code")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToReadOutsideWorkspace")
@ -328,19 +371,45 @@ export const ChatRowContent = ({
: t("chat:fileOperations.didRead")}
</span>
</div>
<div className="rounded-[3px] overflow-hidden border border-vscode-editorGroup-border bg-vscode-code-block-background">
<div
style={{
borderRadius: 3,
backgroundColor: CODE_BLOCK_BG_COLOR,
overflow: "hidden",
border: "1px solid var(--vscode-editorGroup-border)",
}}>
<div
className="text-vscode-descriptionForeground flex items-center p-[9px_10px] cursor-pointer select-none"
style={{
color: "var(--vscode-descriptionForeground)",
display: "flex",
alignItems: "center",
padding: "9px 10px",
cursor: "pointer",
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
onClick={() => {
vscode.postMessage({ type: "openFile", text: tool.content })
}}>
{tool.path?.startsWith(".") && <span>.</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2 rtl text-left">
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
{tool.reason}
</span>
<div className="flex-grow"></div>
<span className="codicon codicon-link-external text-[13.5px] m-[1px_0]"></span>
<div style={{ flexGrow: 1 }}></div>
<span
className={`codicon codicon-link-external`}
style={{ fontSize: 13.5, margin: "1px 0" }}></span>
</div>
</div>
</>
@ -348,9 +417,9 @@ export const ChatRowContent = ({
case "fetchInstructions":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("file-code")}
<span className="font-bold">{t("chat:instructions.wantsToFetch")}</span>
<span style={{ fontWeight: "bold" }}>{t("chat:instructions.wantsToFetch")}</span>
</div>
<CodeAccordian
isLoading={message.partial}
@ -363,9 +432,9 @@ export const ChatRowContent = ({
case "listFilesTopLevel":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("folder-opened")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:directoryOperations.wantsToViewTopLevel")
: t("chat:directoryOperations.didViewTopLevel")}
@ -383,9 +452,9 @@ export const ChatRowContent = ({
case "listFilesRecursive":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("folder-opened")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:directoryOperations.wantsToViewRecursive")
: t("chat:directoryOperations.didViewRecursive")}
@ -403,9 +472,9 @@ export const ChatRowContent = ({
case "listCodeDefinitionNames":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("file-code")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:directoryOperations.wantsToViewDefinitions")
: t("chat:directoryOperations.didViewDefinitions")}
@ -422,9 +491,9 @@ export const ChatRowContent = ({
case "searchFiles":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("search")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? (
<Trans
i18nKey="chat:directoryOperations.wantsToSearch"
@ -452,9 +521,9 @@ export const ChatRowContent = ({
case "switchMode":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("symbol-enum")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? (
<>
{tool.reason ? (
@ -495,9 +564,9 @@ export const ChatRowContent = ({
case "newTask":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("tasklist")}
<span className="font-bold">
<span style={{ fontWeight: "bold" }}>
<Trans
i18nKey="chat:subtasks.wantsToCreate"
components={{ code: <code>{tool.mode}</code> }}
@ -538,9 +607,9 @@ export const ChatRowContent = ({
case "finishTask":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{toolIcon("check-all")}
<span className="font-bold">{t("chat:subtasks.wantsToFinish")}</span>
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.wantsToFinish")}</span>
</div>
<div
style={{
@ -719,16 +788,22 @@ export const ChatRowContent = ({
return (
<>
<div
className={cn(
headerClasses,
"justify-between cursor-pointer select-none",
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
style={{
...headerStyle,
marginBottom:
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
apiReqStreamingFailedMessage
? "mb-[10px]"
: "mb-0",
)}
? 10
: 0,
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
onClick={onToggleExpand}>
<div className="flex items-center gap-[10px] flex-grow">
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
{icon}
{title}
<VSCodeBadge
@ -741,7 +816,7 @@ export const ChatRowContent = ({
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
apiReqStreamingFailedMessage) && (
<>
<p className={cn(pClasses, "text-vscode-errorForeground")}>
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
@ -823,22 +898,22 @@ export const ChatRowContent = ({
return (
<>
{title && (
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<p className={cn(pClasses, "text-vscode-errorForeground")}>{message.text}</p>
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
</>
)
case "completion_result":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
<div className="text-vscode-charts-green pt-[10px]">
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
<Markdown markdown={message.text} />
</div>
</>
@ -848,8 +923,16 @@ export const ChatRowContent = ({
case "mcp_server_response":
return (
<>
<div className="pt-0">
<div className="mb-1 opacity-80 text-xs uppercase">{t("chat:response")}</div>
<div style={{ paddingTop: 0 }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
{t("chat:response")}
</div>
<CodeAccordian
code={message.text}
language="json"
@ -872,12 +955,12 @@ export const ChatRowContent = ({
return (
<>
{title && (
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<div className="pt-[10px]">
<div style={{ paddingTop: 10 }}>
<Markdown markdown={message.text} partial={message.partial} />
</div>
</>
@ -888,17 +971,17 @@ export const ChatRowContent = ({
case "mistake_limit_reached":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
<p className={cn(pClasses, "text-vscode-errorForeground")}>{message.text}</p>
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
</>
)
case "command":
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
@ -916,11 +999,17 @@ export const ChatRowContent = ({
return (
<>
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
<div className="bg-vscode-textCodeBlock-background rounded-[3px] p-[8px_10px] mt-2">
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "3px",
padding: "8px 10px",
marginTop: "8px",
}}>
{useMcpServer.type === "access_mcp_resource" && (
<McpResourceRow
item={{
@ -959,8 +1048,14 @@ export const ChatRowContent = ({
/>
</div>
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
<div className="mt-2">
<div className="mb-1 opacity-80 text-xs uppercase">
<div style={{ marginTop: "8px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
{t("chat:arguments")}
</div>
<CodeAccordian
@ -980,11 +1075,11 @@ export const ChatRowContent = ({
if (message.text) {
return (
<div>
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
<div className="text-vscode-charts-green pt-[10px]">
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
<Markdown markdown={message.text} partial={message.partial} />
</div>
</div>
@ -996,15 +1091,15 @@ export const ChatRowContent = ({
return (
<>
{title && (
<div className={headerClasses}>
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<div className="pt-[10px] pb-[15px]">
<p className={pClasses}>
{message.partial === true ? message?.text : followUpData?.question}
</p>
<div style={{ paddingTop: 10, paddingBottom: 15 }}>
<Markdown
markdown={message.partial === true ? message?.text : followUpData?.question}
/>
</div>
<FollowUpSuggest
suggestions={followUpData?.suggest}

View file

@ -40,7 +40,6 @@ import TaskHeader from "./TaskHeader"
import AutoApproveMenu from "./AutoApproveMenu"
import SystemPromptWarning from "./SystemPromptWarning"
import { CheckpointWarning } from "./CheckpointWarning"
import { cn } from "@/lib/utils"
export interface ChatViewProps {
isHidden: boolean
@ -1249,10 +1248,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</div>
)}
<div
className={cn(
"w-full flex flex-col gap-4 m-auto px-3.5 min-[370px]:px-10 pt-5 transition-all duration-300",
isExpanded && tasks.length > 0 ? "mt-0" : "",
)}>
className={` w-full flex flex-col gap-4 m-auto ${isExpanded && tasks.length > 0 ? "mt-0" : ""} px-3.5 min-[370px]:px-10 pt-5 transition-all duration-300`}>
<RooHero />
{telemetrySetting === "unset" && <TelemetryBanner />}
{/* Show the task history preview if expanded and tasks exist */}
@ -1262,11 +1258,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
i18nKey="chat:about"
components={{
DocsLink: (
<a
href="https://docs.roocode.com/"
target="_blank"
rel="noopener noreferrer"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline">
<a href="https://docs.roocode.com/" target="_blank" rel="noopener noreferrer">
the docs
</a>
),
@ -1328,7 +1320,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
{showScrollToBottom ? (
<div className="flex px-[15px] pt-[10px]">
<div
className="bg-vscode-toolbar-hoverBackground/55 rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-vscode-toolbar-hoverBackground/90 active:bg-vscode-toolbar-hoverBackground/70"
className="bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_55%,_transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_90%,_transparent)] active:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_70%,_transparent)]"
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false

View file

@ -27,7 +27,10 @@ export const CommandExecutionError = () => {
}}
/>
</div>
<a href="http://docs.roocode.com/troubleshooting/shell-integration/" className="underline text-inherit">
<a
href="http://docs.roocode.com/troubleshooting/shell-integration/"
className="underline"
style={{ color: "inherit" }}>
{t("chat:shellIntegration.troubleshooting")}
</a>
</div>

View file

@ -68,10 +68,18 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
switch (option.type) {
case ContextMenuOptionType.Mode:
return (
<div className="flex flex-col gap-0.5">
<span className="leading-tight">{option.label}</span>
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
{option.description && (
<span className="opacity-50 text-[0.9em] leading-tight whitespace-nowrap overflow-hidden text-ellipsis">
<span
style={{
opacity: 0.5,
fontSize: "0.9em",
lineHeight: "1.2",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{option.description}
</span>
)}
@ -88,9 +96,17 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
case ContextMenuOptionType.Git:
if (option.value) {
return (
<div className="flex flex-col">
<span className="leading-tight">{option.label}</span>
<span className="text-[0.85em] opacity-70 whitespace-nowrap overflow-hidden text-ellipsis leading-tight">
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
<span
style={{
fontSize: "0.85em",
opacity: 0.7,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
lineHeight: "1.2",
}}>
{option.description}
</span>
</div>
@ -108,9 +124,29 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
const filename = pathList.at(-1)
const folderPath = pathList.slice(0, -1).join("/")
return (
<div className="flex-1 overflow-hidden flex gap-[0.5em] whitespace-nowrap items-center justify-between text-left">
<div
style={{
flex: 1,
overflow: "hidden",
display: "flex",
gap: "0.5em",
whiteSpace: "nowrap",
alignItems: "center",
justifyContent: "space-between",
textAlign: "left",
}}>
<span>{filename}</span>
<span className="whitespace-nowrap overflow-hidden text-ellipsis rtl text-right flex-1 opacity-75 text-[0.75em]">
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
direction: "rtl",
textAlign: "right",
flex: 1,
opacity: 0.75,
fontSize: "0.75em",
}}>
{folderPath}
</span>
</div>
@ -161,32 +197,68 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
return (
<div
className="absolute bottom-[calc(100%-10px)] left-[15px] right-[15px] overflow-x-hidden"
style={{
position: "absolute",
bottom: "calc(100% - 10px)",
left: 15,
right: 15,
overflowX: "hidden",
}}
onMouseDown={onMouseDown}>
<div
ref={menuRef}
className="bg-vscode-dropdown-background border border-vscode-editorGroup-border rounded-[3px] shadow-[0_4px_10px_rgba(0,0,0,0.25)] z-[1000] flex flex-col max-h-[200px] overflow-y-auto">
style={{
backgroundColor: "var(--vscode-dropdown-background)",
border: "1px solid var(--vscode-editorGroup-border)",
borderRadius: "3px",
boxShadow: "0 4px 10px rgba(0, 0, 0, 0.25)",
zIndex: 1000,
display: "flex",
flexDirection: "column",
maxHeight: "200px",
overflowY: "auto",
}}>
{filteredOptions && filteredOptions.length > 0 ? (
filteredOptions.map((option, index) => (
<div
key={`${option.type}-${option.value || index}`}
onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)}
className={`p-[4px_6px] text-vscode-dropdown-foreground flex items-center justify-between ${
isOptionSelectable(option) ? "cursor-pointer" : "cursor-default"
} ${
index === selectedIndex && isOptionSelectable(option)
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
: ""
}`}
style={{
padding: "4px 6px",
cursor: isOptionSelectable(option) ? "pointer" : "default",
color: "var(--vscode-dropdown-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
...(index === selectedIndex && isOptionSelectable(option)
? {
backgroundColor: "var(--vscode-list-activeSelectionBackground)",
color: "var(--vscode-list-activeSelectionForeground)",
}
: {}),
}}
onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}>
<div className="flex items-center flex-1 min-w-0 overflow-hidden pt-0">
<div
style={{
display: "flex",
alignItems: "center",
flex: 1,
minWidth: 0,
overflow: "hidden",
paddingTop: 0,
}}>
{(option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.OpenedFile) && (
<img
src={getMaterialIconForOption(option)}
alt="Mode"
className="mr-[6px] flex-shrink-0 w-4 h-4"
style={{
marginRight: "6px",
flexShrink: 0,
width: "16px",
height: "16px",
}}
/>
)}
{option.type !== ContextMenuOptionType.Mode &&
@ -195,9 +267,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
option.type !== ContextMenuOptionType.OpenedFile &&
getIconForOption(option) && (
<i
className={`codicon codicon-${getIconForOption(
option,
)} mr-[6px] flex-shrink-0 text-sm mt-0`}
className={`codicon codicon-${getIconForOption(option)}`}
style={{
marginRight: "6px",
flexShrink: 0,
fontSize: "14px",
marginTop: 0,
}}
/>
)}
{renderOptionContent(option)}
@ -206,12 +282,23 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.Git) &&
!option.value && (
<i className="codicon codicon-chevron-right text-[10px] flex-shrink-0 ml-2" />
<i
className="codicon codicon-chevron-right"
style={{ fontSize: "10px", flexShrink: 0, marginLeft: 8 }}
/>
)}
</div>
))
) : (
<div className="p-1 flex items-center justify-center text-vscode-foreground opacity-70">
<div
style={{
padding: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--vscode-foreground)",
opacity: 0.7,
}}>
<span>No results found</span>
</div>
)}
@ -219,4 +306,5 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
</div>
)
}
export default ContextMenu

View file

@ -38,9 +38,9 @@ export const IconButton: React.FC<IconButtonProps> = ({
<button
aria-label={title}
title={title}
className={`${buttonClasses} text-[16.5px]`}
className={buttonClasses}
onClick={!disabled ? onClick : undefined}
style={style}
style={{ fontSize: 16.5, ...style }}
{...props}>
<span className={iconClasses} />
</button>

View file

@ -1,14 +1,12 @@
import { memo, useState } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@src/utils/clipboard"
import MarkdownBlock from "../common/MarkdownBlock"
export const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
const [isHovering, setIsHovering] = useState(false)
const [copySuccess, setCopySuccess] = useState(false)
// Shorter feedback duration for copy button flash.
const { copyWithFeedback } = useCopyToClipboard(200)
@ -18,25 +16,43 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia
}
return (
<div onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} className="relative">
<div className="break-words overflow-wrap-anywhere mb-[-15px] mt-[-15px]">
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
style={{ position: "relative" }}>
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
<MarkdownBlock markdown={markdown} />
</div>
{markdown && !partial && isHovering && (
<div className="absolute bottom-[-4px] right-2 opacity-0 rounded animate-fadeIn duration-200 ease-in-out forwards">
<div
style={{
position: "absolute",
bottom: "-4px",
right: "8px",
opacity: 0,
animation: "fadeIn 0.2s ease-in-out forwards",
borderRadius: "4px",
}}>
<style>{`@keyframes fadeIn { from { opacity: 0; } to { opacity: 1.0; } }`}</style>
<VSCodeButton
className={cn(
"copy-button h-6 border-none bg-vscode-editor-background transition-colors duration-200 ease-in-out",
copySuccess && "bg-vscode-button-background",
)}
className="copy-button"
appearance="icon"
style={{
height: "24px",
border: "none",
background: "var(--vscode-editor-background)",
transition: "background 0.2s ease-in-out",
}}
onClick={async () => {
const success = await copyWithFeedback(markdown)
if (success) {
setCopySuccess(true)
setTimeout(() => {
setCopySuccess(false)
}, 200)
const button = document.activeElement as HTMLElement
if (button) {
button.style.background = "var(--vscode-button-background)"
setTimeout(() => {
button.style.background = ""
}, 200)
}
}
}}
title="Copy as markdown">

View file

@ -1,8 +1,15 @@
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
export const ProgressIndicator = () => (
<div className="w-4 h-4 flex items-center justify-center">
<div className="scale-[.55] origin-center">
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
<VSCodeProgressRing />
</div>
</div>

View file

@ -1,7 +1,6 @@
import { memo, useMemo } from "react"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { cn } from "@/lib/utils"
import CodeBlock from "./CodeBlock"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
import { ToolProgressStatus } from "@roo/shared/ExtensionMessage"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
@ -47,31 +46,64 @@ const CodeAccordian = ({
)
return (
<div className="rounded-[3px] overflow-hidden border border-vscode-editorGroup-border bg-vscode-code-block-background">
<div
style={{
borderRadius: 3,
backgroundColor: CODE_BLOCK_BG_COLOR,
overflow: "hidden", // This ensures the inner scrollable area doesn't overflow the rounded corners
border: "1px solid var(--vscode-editorGroup-border)",
}}>
{(path || isFeedback || isConsoleLogs) && (
<div
className={cn(
"text-vscode-descriptionForeground flex items-center p-[9px_10px] select-none",
isLoading ? "cursor-wait opacity-70 animate-pulse" : "cursor-pointer opacity-100",
)}
style={{
color: "var(--vscode-descriptionForeground)",
display: "flex",
alignItems: "center",
padding: "9px 10px",
cursor: isLoading ? "wait" : "pointer",
opacity: isLoading ? 0.7 : 1,
// pointerEvents: isLoading ? "none" : "auto",
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
className={`${isLoading ? "animate-pulse" : ""}`}
onClick={isLoading ? undefined : onToggleExpand}>
{isLoading && <VSCodeProgressRing className="size-3 mr-2" />}
{isFeedback || isConsoleLogs ? (
<div className="flex items-center">
<span className={`codicon codicon-${isFeedback ? "feedback" : "output"} mr-[6px]`}></span>
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2">
<div style={{ display: "flex", alignItems: "center" }}>
<span
className={`codicon codicon-${isFeedback ? "feedback" : "output"}`}
style={{ marginRight: "6px" }}></span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
}}>
{isFeedback ? "User Edits" : "Console Logs"}
</span>
</div>
) : (
<>
{path?.startsWith(".") && <span>.</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2 rtl text-left">
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
// trick to get ellipsis at beginning of string
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(path ?? "") + "\u200E"}
</span>
</>
)}
<div className="flex-grow"></div>
<div style={{ flexGrow: 1 }}></div>
{progressStatus && progressStatus.text && (
<>
{progressStatus.icon && <span className={`codicon codicon-${progressStatus.icon} mr-1`} />}
@ -84,7 +116,12 @@ const CodeAccordian = ({
</div>
)}
{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (
<div className="overflow-x-auto overflow-y-hidden max-w-full">
<div
style={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
}}>
<CodeBlock
source={(code ?? diff ?? "").trim()}
language={diff !== undefined ? "diff" : inferredLanguage}

View file

@ -1,12 +1,12 @@
import React, { memo, useEffect, useRef, useCallback, useState } from "react"
import { memo, useEffect, useRef, useCallback, useState } from "react"
import styled from "styled-components"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { getHighlighter, isLanguageLoaded, normalizeLanguage, ExtendedLanguage } from "@src/utils/highlighter"
import { bundledLanguages } from "shiki"
import type { ShikiTransformer } from "shiki"
import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { cn } from "@/lib/utils"
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
export const WRAPPER_ALPHA = "cc" // 80% opacity
// Configuration constants
export const WINDOW_SHADE_SETTINGS = {
@ -36,116 +36,179 @@ interface CodeBlockProps {
onLanguageChange?: (language: string) => void
}
interface CodeBlockButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode
}
const CodeBlockButton = styled.button`
background: transparent;
border: none;
color: var(--vscode-foreground);
cursor: var(--copy-button-cursor, default);
padding: 4px;
margin: 0 0px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.4;
border-radius: 3px;
pointer-events: var(--copy-button-events, none);
margin-left: 4px;
height: 24px;
width: 24px;
const CodeBlockButton = ({ children, ...props }: CodeBlockButtonProps) => {
return (
<button
className="bg-transparent border-none text-vscode-foreground p-1 mx-0 flex items-center justify-center opacity-40 rounded-[3px] ml-1 h-6 w-6 hover:bg-vscode-toolbar-hoverBackground hover:opacity-100"
style={{
cursor: "var(--copy-button-cursor, default)",
pointerEvents: "var(--copy-button-events, none)" as any,
}}
{...props}>
{children}
</button>
)
}
&:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
interface CodeBlockButtonWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
}
/* Style for Lucide icons to ensure consistent sizing and positioning */
svg {
display: block;
}
`
const CodeBlockButtonWrapper = React.forwardRef<HTMLDivElement, CodeBlockButtonWrapperProps>(
({ children, ...props }, ref) => {
return (
<div
ref={ref}
className="fixed h-auto z-[100] overflow-visible pointer-events-none p-[4px_6px] rounded-[3px] inline-flex items-center justify-center hover:bg-vscode-editor-background hover:!opacity-100 bg-vscode-editor-background/[.80]"
style={{
top: "var(--copy-button-top)",
right: "var(--copy-button-right, 8px)",
opacity: "var(--copy-button-opacity, 0)",
}}
{...props}>
{children}
</div>
)
},
)
CodeBlockButtonWrapper.displayName = "CodeBlockButtonWrapper"
const CodeBlockButtonWrapper = styled.div`
position: fixed;
top: var(--copy-button-top);
right: var(--copy-button-right, 8px);
height: auto;
z-index: 100;
background: ${CODE_BLOCK_BG_COLOR}${WRAPPER_ALPHA};
overflow: visible;
pointer-events: none;
opacity: var(--copy-button-opacity, 0);
padding: 4px 6px;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
interface CodeBlockContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
"data-partially-visible"?: boolean
}
&:hover {
background: var(--vscode-editor-background);
opacity: 1 !important;
}
const CodeBlockContainer = React.forwardRef<HTMLDivElement, CodeBlockContainerProps>(
({ children, "data-partially-visible": partiallyVisible, ...props }, ref) => {
return (
<div
ref={ref}
className="relative overflow-hidden border-b-4 border-vscode-sideBar-background bg-background"
data-partially-visible={partiallyVisible}
{...props}>
{children}
</div>
)
},
)
CodeBlockContainer.displayName = "CodeBlockContainer"
${CodeBlockButton} {
position: relative;
top: 0;
right: 0;
}
`
interface StyledPreProps extends React.HTMLAttributes<HTMLDivElement> {
const CodeBlockContainer = styled.div`
position: relative;
overflow: hidden;
border-bottom: 4px solid var(--vscode-sideBar-background);
background-color: ${CODE_BLOCK_BG_COLOR};
${CodeBlockButtonWrapper} {
opacity: 0;
pointer-events: none;
transition: opacity 0.2s; /* Keep opacity transition for buttons */
}
&[data-partially-visible="true"]:hover ${CodeBlockButtonWrapper} {
opacity: 1;
pointer-events: all;
cursor: pointer;
}
`
export const StyledPre = styled.div<{
preStyle?: React.CSSProperties
wordwrap?: "true" | "false" | undefined
windowshade?: "true" | "false"
collapsedHeight?: number
children: React.ReactNode
}
}>`
background-color: ${CODE_BLOCK_BG_COLOR};
max-height: ${({ windowshade, collapsedHeight }) =>
windowshade === "true" ? `${collapsedHeight || WINDOW_SHADE_SETTINGS.collapsedHeight}px` : "none"};
overflow-y: auto;
padding: 10px;
// transition: max-height ${WINDOW_SHADE_SETTINGS.transitionDelayS} ease-out;
border-radius: 5px;
${({ preStyle }) => preStyle && { ...preStyle }}
export const StyledPre = React.forwardRef<HTMLDivElement, StyledPreProps>(
({ preStyle, wordwrap, windowshade, collapsedHeight, children, className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"overflow-y-auto p-[10px] rounded-[5px]",
"text-vscode-editor-font-size font-vscode-editor-font-family",
"bg-background",
windowshade === "true"
? `[max-height:${collapsedHeight || WINDOW_SHADE_SETTINGS.collapsedHeight}px]`
: "max-h-none",
wordwrap === "false" ? "whitespace-pre" : "whitespace-pre-wrap",
"break-normal",
wordwrap === "false" ? "[overflow-wrap:normal]" : "break-words",
className,
)}
style={preStyle}
{...props}>
{children}
</div>
)
},
)
StyledPre.displayName = "StyledPre"
pre {
background-color: ${CODE_BLOCK_BG_COLOR};
border-radius: 5px;
margin: 0;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
interface LanguageSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
children: React.ReactNode
}
const LanguageSelect = ({ children, className, ...props }: LanguageSelectProps) => {
return (
<select
className={cn(
"text-xs text-vscode-foreground opacity-40 font-mono appearance-none bg-transparent border-none cursor-pointer p-1 m-0 align-middle h-6 hover:opacity-100 hover:bg-vscode-toolbar-hoverBackground hover:rounded-[3px] focus:opacity-100 focus:outline-none focus:rounded-[3px]",
className,
)}
{...props}>
{children}
</select>
)
}
pre,
code {
/* Undefined wordwrap defaults to true (pre-wrap) behavior */
white-space: ${({ wordwrap }) => (wordwrap === "false" ? "pre" : "pre-wrap")};
word-break: ${({ wordwrap }) => (wordwrap === "false" ? "normal" : "normal")};
overflow-wrap: ${({ wordwrap }) => (wordwrap === "false" ? "normal" : "break-word")};
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
font-family: var(--vscode-editor-font-family);
}
pre > code {
.hljs-deletion {
background-color: var(--vscode-diffEditor-removedTextBackground);
display: inline-block;
width: 100%;
}
.hljs-addition {
background-color: var(--vscode-diffEditor-insertedTextBackground);
display: inline-block;
width: 100%;
}
}
.hljs {
color: var(--vscode-editor-foreground, #fff);
background-color: ${CODE_BLOCK_BG_COLOR};
}
`
const LanguageSelect = styled.select`
font-size: 12px;
color: var(--vscode-foreground);
opacity: 0.4;
font-family: monospace;
appearance: none;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
margin: 0;
vertical-align: middle;
height: 24px;
& option {
background: var(--vscode-editor-background);
color: var(--vscode-foreground);
padding: 0;
margin: 0;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background);
}
&::-webkit-scrollbar-track {
background: var(--vscode-editor-background);
}
&:hover {
opacity: 1;
background: var(--vscode-toolbar-hoverBackground);
border-radius: 3px;
}
&:focus {
opacity: 1;
outline: none;
border-radius: 3px;
}
`
const CodeBlock = memo(
({
@ -180,7 +243,7 @@ const CodeBlock = memo(
// Syntax highlighting with cached Shiki instance
useEffect(() => {
const fallback = `<pre class="p-0 m-0"><code class="hljs language-${currentLanguage || "txt"}">${source || ""}</code></pre>`
const fallback = `<pre style="padding: 0; margin: 0;"><code class="hljs language-${currentLanguage || "txt"}">${source || ""}</code></pre>`
const highlight = async () => {
// Show plain text if language needs to be loaded
if (currentLanguage && !isLanguageLoaded(currentLanguage)) {
@ -194,7 +257,7 @@ const CodeBlock = memo(
transformers: [
{
pre(node) {
node.properties.class = "p-0 m-0"
node.properties.style = "padding: 0; margin: 0;"
return node
},
code(node) {
@ -557,7 +620,7 @@ const CodeBlock = memo(
<CodeBlockButtonWrapper
ref={copyButtonWrapperRef}
onMouseOver={() => updateCodeBlockButtonPosition()}
className="gap-0">
style={{ gap: 0 }}>
{language && (
<LanguageSelect
value={currentLanguage}
@ -580,7 +643,7 @@ const CodeBlock = memo(
language && (
<option
value={normalizeLanguage(language)}
className="font-bold text-left text-[1.2em]">
style={{ fontWeight: "bold", textAlign: "left", fontSize: "1.2em" }}>
{normalizeLanguage(language)}
</option>
)
@ -595,12 +658,13 @@ const CodeBlock = memo(
<option
key={normalizedLang}
value={normalizedLang}
className={cn(
"text-left",
normalizedLang === currentLanguage
? "font-bold text-[1.2em]"
: "font-normal",
)}>
style={{
fontWeight:
normalizedLang === currentLanguage ? "bold" : "normal",
textAlign: "left",
fontSize:
normalizedLang === currentLanguage ? "1.2em" : "inherit",
}}>
{normalizedLang}
</option>
)

View file

@ -1,5 +1,6 @@
import React, { memo, useEffect, FC, PropsWithChildren } from "react"
import React, { memo, useEffect } from "react"
import { useRemark } from "react-remark"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { vscode } from "@src/utils/vscode"
@ -54,45 +55,71 @@ const remarkUrlToLink = () => {
}
}
const StyledMarkdown: FC<PropsWithChildren<unknown>> = ({ children }) => {
// Note: Tailwind doesn't have a direct equivalent for targeting based on body data attributes like `body[data-vscode-theme-kind="vscode-high-contrast"]`.
// This specific high-contrast styling might need to be handled differently, possibly via a theme-aware context or by adding a class to this component when high contrast is active.
// For now, the general styles are applied.
return (
<div
className="
font-vscode-font-family
text-vscode-font-size
[&_p]:leading-tight
[&_li]:leading-tight
[&_ol]:leading-tight
[&_ul]:leading-tight
[&_ol]:pl-[2.5em]
[&_ul]:pl-[2.5em]
[&_ol]:ml-0
[&_ul]:ml-0
[&_p]:whitespace-pre-wrap
[&_a]:text-vscode-textLink-foreground
[&_a]:underline
[&_a]:decoration-dotted
[&_a]:decoration-vscode-textLink-foreground
hover:[&_a]:text-vscode-textLink-activeForeground
hover:[&_a]:decoration-solid
hover:[&_a]:decoration-vscode-textLink-activeForeground
[&_code:not(pre>code)]:font-vscode-editor-font-family
[&_code:not(pre>code)]:saturate-[1.1]
[&_code:not(pre>code)]:brightness-95
[&_code:not(pre>code)]:text-vscode-textPreformat-foreground!
[&_code:not(pre>code)]:bg-vscode-textPreformat-background!
[&_code:not(pre>code)]:px-[2px]
[&_code:not(pre>code)]:whitespace-pre-line
[&_code:not(pre>code)]:break-words
[&_code:not(pre>code)]:overflow-wrap-anywhere
">
{children}
</div>
)
}
const StyledMarkdown = styled.div`
code:not(pre > code) {
font-family: var(--vscode-editor-font-family, monospace);
filter: saturation(110%) brightness(95%);
color: var(--vscode-textPreformat-foreground) !important;
background-color: var(--vscode-textPreformat-background) !important;
padding: 0px 2px;
white-space: pre-line;
word-break: break-word;
overflow-wrap: anywhere;
}
/* Target only Dark High Contrast theme using the data attribute VS Code adds to the body */
body[data-vscode-theme-kind="vscode-high-contrast"] & code:not(pre > code) {
color: var(
--vscode-editorInlayHint-foreground,
var(--vscode-symbolIcon-stringForeground, var(--vscode-charts-orange, #e9a700))
);
}
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: var(--vscode-font-size, 13px);
p,
li,
ol,
ul {
line-height: 1.25;
}
ol,
ul {
padding-left: 2.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
color: var(--vscode-textLink-foreground);
text-decoration-line: underline;
text-decoration-style: dotted;
text-decoration-color: var(--vscode-textLink-foreground);
&:hover {
color: var(--vscode-textLink-activeForeground);
text-decoration-style: solid;
text-decoration-color: var(--vscode-textLink-activeForeground);
}
}
`
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const { theme } = useExtensionState()
@ -201,7 +228,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
}, [markdown, setMarkdown, theme])
return (
<div>
<div style={{}}>
<StyledMarkdown>{reactContent}</StyledMarkdown>
</div>
)

View file

@ -1,11 +1,11 @@
import React, { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import mermaid from "mermaid"
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useCopyToClipboard } from "@src/utils/clipboard"
import CodeBlock from "./CodeBlock"
import { cn } from "@/lib/utils"
const MERMAID_THEME = {
background: "#1e1e1e", // VS Code dark theme background
@ -152,18 +152,37 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
{error ? (
<div className="mt-0 overflow-hidden mb-2">
<div style={{ marginTop: "0px", overflow: "hidden", marginBottom: "8px" }}>
<div
className={cn(
"font-normal text-vscode-font-size text-vscode-editor-foreground flex items-center justify-between cursor-pointer",
isErrorExpanded ? "border-b border-vscode-editorGroup-border" : "border-b-0",
)}
style={{
borderBottom: isErrorExpanded ? "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={() => setIsErrorExpanded(!isErrorExpanded)}>
<div className="flex items-center gap-2.5 flex-grow">
<span className="codicon codicon-warning text-vscode-editorWarning-foreground opacity-80 text-base mb-[-1.5px]"></span>
<span className="font-bold">{t("common:mermaid.render_error")}</span>
<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("common:mermaid.render_error")}</span>
</div>
<div className="flex items-center">
<div style={{ display: "flex", alignItems: "center" }}>
<CopyButton
onClick={(e) => {
e.stopPropagation()
@ -176,14 +195,21 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
</div>
</div>
{isErrorExpanded && (
<div className="p-2 bg-vscode-editor-background border-t-0">
<div className="mb-2 text-vscode-descriptionForeground">{error}</div>
<div
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
borderTop: "none",
}}>
<div style={{ marginBottom: "8px", color: "var(--vscode-descriptionForeground)" }}>
{error}
</div>
<CodeBlock language="mermaid" source={code} />
</div>
)}
</div>
) : (
<SvgContainer onClick={handleClick} ref={containerRef} isLoading={isLoading} />
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
)}
</MermaidBlockContainer>
)
@ -240,40 +266,44 @@ async function svgToPng(svgEl: SVGElement): Promise<string> {
})
}
const MermaidBlockContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="relative my-2">{children}</div>
const MermaidBlockContainer = styled.div`
position: relative;
margin: 8px 0;
`
const LoadingMessage = styled.div`
padding: 8px 0;
color: var(--vscode-descriptionForeground);
font-style: italic;
font-size: 0.9em;
`
const CopyButton = styled.button`
padding: 3px;
height: 24px;
margin-right: 4px;
color: var(--vscode-editor-foreground);
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`
interface SvgContainerProps {
$isLoading: boolean
}
const LoadingMessage: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div className="py-2 text-vscode-descriptionForeground italic text-[0.9em]">{children}</div>
}
const CopyButton: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>> = ({ children, ...props }) => {
return (
<button
className="p-[3px] h-6 mr-1 text-vscode-editor-foreground flex items-center justify-center bg-transparent border-none cursor-pointer hover:opacity-80"
{...props}>
{children}
</button>
)
}
interface SvgContainerProps extends React.HTMLAttributes<HTMLDivElement> {
isLoading: boolean
children?: React.ReactNode
}
const SvgContainer = React.forwardRef<HTMLDivElement, SvgContainerProps>(({ isLoading, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"min-h-[20px] transition-opacity duration-200 ease-in-out cursor-pointer flex justify-center",
isLoading ? "opacity-30" : "opacity-100",
)}
{...props}>
{children}
</div>
)
})
SvgContainer.displayName = "SvgContainer"
const SvgContainer = styled.div<SvgContainerProps>`
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
min-height: 20px;
transition: opacity 0.2s ease;
cursor: pointer;
display: flex;
justify-content: center;
`

View file

@ -1,10 +1,29 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useState } from "react"
import styled from "styled-components"
import { vscode } from "@src/utils/vscode"
import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Trans } from "react-i18next"
import { cn } from "@/lib/utils"
const BannerContainer = styled.div`
background-color: var(--vscode-banner-background);
padding: 12px 20px;
display: flex;
flex-direction: column;
gap: 10px;
flex-shrink: 0;
margin-bottom: 6px;
`
const ButtonContainer = styled.div`
display: flex;
gap: 8px;
width: 100%;
& > vscode-button {
flex: 1;
}
`
const TelemetryBanner = () => {
const { t } = useAppTranslation()
@ -29,7 +48,7 @@ const TelemetryBanner = () => {
}
return (
<div className={`bg-vscode-banner-background p-[12px_20px] flex flex-col gap-[10px] shrink-0 mb-[6px]`}>
<BannerContainer>
<div>
<strong>{t("welcome:telemetry.title")}</strong>
<div className="mt-1">
@ -45,15 +64,15 @@ const TelemetryBanner = () => {
</div>
</div>
</div>
<div className={cn("flex gap-[8px] w-full")}>
<VSCodeButton appearance="primary" onClick={handleAllow} disabled={hasChosen} className="flex-1">
<ButtonContainer>
<VSCodeButton appearance="primary" onClick={handleAllow} disabled={hasChosen}>
{t("welcome:telemetry.allow")}
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleDeny} disabled={hasChosen} className="flex-1">
<VSCodeButton appearance="secondary" onClick={handleDeny} disabled={hasChosen}>
{t("welcome:telemetry.deny")}
</VSCodeButton>
</div>
</div>
</ButtonContainer>
</BannerContainer>
)
}

View file

@ -37,24 +37,56 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
}
return (
<div ref={containerRef} className="flex flex-wrap gap-[5px] gap-y-[3px]" style={style}>
<div
ref={containerRef}
style={{
display: "flex",
flexWrap: "wrap",
gap: 5,
rowGap: 3,
...style,
}}>
{images.map((image, index) => (
<div
key={index}
className="relative"
style={{ position: "relative" }}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}>
<img
src={image}
alt={`Thumbnail ${index + 1}`}
className="w-[34px] h-[34px] object-cover rounded cursor-pointer"
style={{
width: 34,
height: 34,
objectFit: "cover",
borderRadius: 4,
cursor: "pointer",
}}
onClick={() => handleImageClick(image)}
/>
{isDeletable && hoveredIndex === index && (
<div
onClick={() => handleDelete(index)}
className="absolute -top-1 -right-1 w-[13px] h-[13px] rounded-full bg-vscode-badge-background flex justify-center items-center cursor-pointer">
<span className="codicon codicon-close text-vscode-foreground text-[10px] font-bold"></span>
style={{
position: "absolute",
top: -4,
right: -4,
width: 13,
height: 13,
borderRadius: "50%",
backgroundColor: "var(--vscode-badge-background)",
display: "flex",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
}}>
<span
className="codicon codicon-close"
style={{
color: "var(--vscode-foreground)",
fontSize: 10,
fontWeight: "bold",
}}></span>
</div>
)}
</div>

View file

@ -8,7 +8,12 @@ interface VSCodeButtonLinkProps {
}
export const VSCodeButtonLink = ({ href, children, ...props }: VSCodeButtonLinkProps) => (
<a href={href} className="no-underline text-inherit">
<a
href={href}
style={{
textDecoration: "none",
color: "inherit",
}}>
<VSCodeButton {...props}>{children}</VSCodeButton>
</a>
)

View file

@ -29,11 +29,13 @@ const HistoryPreview = () => {
<CopyButton itemTask={item.task} />
</div>
<div
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap break-words overflow-wrap-anywhere"
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap"
style={{
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{item.task}
</div>

View file

@ -99,7 +99,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
</div>
<div className="flex flex-col gap-2">
<VSCodeTextField
className="w-full"
style={{ width: "100%" }}
placeholder={t("history:searchPlaceholder")}
value={searchQuery}
data-testid="history-search-input"
@ -111,18 +111,28 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
setSortOption("mostRelevant")
}
}}>
<div slot="start" className="codicon codicon-search text-[13px] mt-[2.5px] opacity-80" />
<div
slot="start"
className="codicon codicon-search"
style={{ fontSize: 13, marginTop: 2.5, opacity: 0.8 }}
/>
{searchQuery && (
<div
className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
className="input-icon-button codicon codicon-close"
aria-label="Clear search"
onClick={() => setSearchQuery("")}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
<VSCodeRadioGroup
className="flex flex-wrap"
style={{ display: "flex", flexWrap: "wrap" }}
value={sortOption}
role="radiogroup"
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
@ -142,7 +152,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
value="mostRelevant"
disabled={!searchQuery}
data-testid="radio-most-relevant"
className={searchQuery ? "opacity-100" : "opacity-50"}>
style={{ opacity: searchQuery ? 1 : 0.5 }}>
{t("history:mostRelevant")}
</VSCodeRadio>
</VSCodeRadioGroup>
@ -187,7 +197,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<TabContent className="p-0">
<Virtuoso
className="flex-grow overflow-y-scroll"
style={{
flexGrow: 1,
overflowY: "scroll",
}}
data={tasks}
data-testid="virtuoso-container"
initialTopMostItemIndex={0}
@ -261,33 +274,76 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
</div>
</div>
<div
className="text-vscode-font-size text-vscode-foreground overflow-hidden whitespace-pre-wrap break-words overflow-wrap-anywhere"
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-foreground)",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
data-testid="task-content"
dangerouslySetInnerHTML={{ __html: item.task }}
/>
<div className="flex flex-col gap-1">
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<div
data-testid="tokens-container"
className="flex justify-between items-center">
<div className="flex items-center gap-1 flex-wrap">
<span className="font-medium text-vscode-descriptionForeground">
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
{t("history:tokensLabel")}
</span>
<span
data-testid="tokens-in"
className="flex items-center gap-[3px] text-vscode-descriptionForeground">
<i className="codicon codicon-arrow-up text-xs font-bold mb-[-2px]" />
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-up"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(item.tokensIn || 0)}
</span>
<span
data-testid="tokens-out"
className="flex items-center gap-[3px] text-vscode-descriptionForeground">
<i className="codicon codicon-arrow-down text-xs font-bold mb-[-2px]" />
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-down"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(item.tokensOut || 0)}
</span>
</div>
@ -302,32 +358,75 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{!!item.cacheWrites && (
<div
data-testid="cache-container"
className="flex items-center gap-1 flex-wrap">
<span className="font-medium text-vscode-descriptionForeground">
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
{t("history:cacheLabel")}
</span>
<span
data-testid="cache-writes"
className="flex items-center gap-[3px] text-vscode-descriptionForeground">
<i className="codicon codicon-database text-xs font-bold mb-[-1px]" />
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(item.cacheWrites || 0)}
</span>
<span
data-testid="cache-reads"
className="flex items-center gap-[3px] text-vscode-descriptionForeground">
<i className="codicon codicon-arrow-right text-xs font-bold mb-0" />
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(item.cacheReads || 0)}
</span>
</div>
)}
{!!item.totalCost && (
<div className="flex justify-between items-center mt-[-2px]">
<div className="flex items-center gap-1">
<span className="font-medium text-vscode-descriptionForeground">
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: -2,
}}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
{t("history:apiCostLabel")}
</span>
<span className="text-vscode-descriptionForeground">
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
${item.totalCost?.toFixed(4)}
</span>
</div>

View file

@ -16,11 +16,18 @@ const McpEnabledToggle = () => {
}
return (
<div className="mb-5">
<div style={{ marginBottom: "20px" }}>
<VSCodeCheckbox checked={mcpEnabled} onChange={handleChange}>
<span className="font-medium">{t("mcp:enableToggle.title")}</span>
<span style={{ fontWeight: "500" }}>{t("mcp:enableToggle.title")}</span>
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-vscode-descriptionForeground">{t("mcp:enableToggle.description")}</p>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
{t("mcp:enableToggle.description")}
</p>
</div>
)
}

View file

@ -9,12 +9,26 @@ const McpResourceRow = ({ item }: McpResourceRowProps) => {
const uri = hasUri ? item.uri : item.uriTemplate
return (
<div key={uri} className="py-[3px]">
<div className="flex items-center mb-1">
<span className={`codicon codicon-symbol-file mr-[6px]`} />
<span className="font-medium break-all">{uri}</span>
<div
key={uri}
style={{
padding: "3px 0",
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: "4px",
}}>
<span className={`codicon codicon-symbol-file`} style={{ marginRight: "6px" }} />
<span style={{ fontWeight: 500, wordBreak: "break-all" }}>{uri}</span>
</div>
<div className="text-xs opacity-80 my-1">
<div
style={{
fontSize: "12px",
opacity: 0.8,
margin: "4px 0",
}}>
{item.name && item.description
? `${item.name}: ${item.description}`
: !item.name && item.description
@ -23,9 +37,18 @@ const McpResourceRow = ({ item }: McpResourceRowProps) => {
? item.name
: "No description"}
</div>
<div className="text-xs">
<span className="opacity-80">Returns </span>
<code className="text-vscode-textPreformat-foreground bg-vscode-textPreformat-background px-1 py-[1px] rounded-[3px]">
<div
style={{
fontSize: "12px",
}}>
<span style={{ opacity: 0.8 }}>Returns </span>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
background: "var(--vscode-textPreformat-background)",
padding: "1px 4px",
borderRadius: "3px",
}}>
{item.mimeType || "Unknown"}
</code>
</div>

View file

@ -24,14 +24,18 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
}
return (
<div key={tool.name} className="py-[3px]">
<div
key={tool.name}
style={{
padding: "3px 0",
}}>
<div
data-testid="tool-row-container"
className="flex items-center justify-between"
style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}
onClick={(e) => e.stopPropagation()}>
<div className="flex items-center">
<span className="codicon codicon-symbol-method mr-[6px]"></span>
<span className="font-medium">{tool.name}</span>
<div style={{ display: "flex", alignItems: "center" }}>
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
{serverName && alwaysAllowMcp && (
<VSCodeCheckbox checked={tool.alwaysAllow} onChange={handleAlwaysAllowChange} data-tool={tool.name}>
@ -39,12 +43,32 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
</VSCodeCheckbox>
)}
</div>
{tool.description && <div className="ml-0 mt-1 opacity-80 text-xs">{tool.description}</div>}
{tool.description && (
<div
style={{
marginLeft: "0px",
marginTop: "4px",
opacity: 0.8,
fontSize: "12px",
}}>
{tool.description}
</div>
)}
{tool.inputSchema &&
"properties" in tool.inputSchema &&
Object.keys(tool.inputSchema.properties as Record<string, any>).length > 0 && (
<div className="mt-2 text-xs rounded-[3px] p-2 border border-vscode-descriptionForeground-transparent-30">
<div className="mb-1 opacity-80 text-[11px] uppercase">{t("mcp:tool.parameters")}</div>
<div
style={{
marginTop: "8px",
fontSize: "12px",
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 30%, transparent)",
borderRadius: "3px",
padding: "8px",
}}>
<div
style={{ marginBottom: "4px", opacity: 0.8, fontSize: "11px", textTransform: "uppercase" }}>
{t("mcp:tool.parameters")}
</div>
{Object.entries(tool.inputSchema.properties as Record<string, any>).map(
([paramName, schema]) => {
const isRequired =
@ -54,12 +78,29 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
tool.inputSchema.required.includes(paramName)
return (
<div key={paramName} className="flex items-baseline mt-1">
<code className="text-vscode-textPreformat-foreground mr-2">
<div
key={paramName}
style={{
display: "flex",
alignItems: "baseline",
marginTop: "4px",
}}>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
marginRight: "8px",
}}>
{paramName}
{isRequired && <span className="text-vscode-errorForeground">*</span>}
{isRequired && (
<span style={{ color: "var(--vscode-errorForeground)" }}>*</span>
)}
</code>
<span className="opacity-80 break-words">
<span
style={{
opacity: 0.8,
overflowWrap: "break-word",
wordBreak: "break-word",
}}>
{schema.description || t("mcp:tool.noDescription")}
</span>
</div>
@ -71,4 +112,5 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolR
</div>
)
}
export default McpToolRow

View file

@ -23,7 +23,6 @@ import {
DialogDescription,
DialogFooter,
} from "@src/components/ui"
import { cn } from "@/lib/utils" // Import cn utility
import { Tab, TabContent, TabHeader } from "../common/Tab"
@ -55,12 +54,20 @@ const McpView = ({ onDone }: McpViewProps) => {
</TabHeader>
<TabContent>
<div className="text-vscode-foreground text-[13px] mb-[10px] mt-[5px]">
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "10px",
marginTop: "5px",
}}>
<Trans i18nKey="mcp:description">
<VSCodeLink href="https://github.com/modelcontextprotocol" className="inline">
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
Model Context Protocol
</VSCodeLink>
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" className="inline">
<VSCodeLink
href="https://github.com/modelcontextprotocol/servers"
style={{ display: "inline" }}>
community-made servers
</VSCodeLink>
</Trans>
@ -70,23 +77,28 @@ const McpView = ({ onDone }: McpViewProps) => {
{mcpEnabled && (
<>
<div className="mb-[15px]">
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={enableMcpServerCreation}
onChange={(e: any) => {
setEnableMcpServerCreation(e.target.checked)
vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked })
}}>
<span className="font-medium">{t("mcp:enableServerCreation.title")}</span>
<span style={{ fontWeight: "500" }}>{t("mcp:enableServerCreation.title")}</span>
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-vscode-descriptionForeground">
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
{t("mcp:enableServerCreation.description")}
</p>
</div>
{/* Server List */}
{servers.length > 0 && (
<div className="flex flex-col gap-[10px]">
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{servers.map((server) => (
<ServerRow
key={`${server.name}-${server.source || "global"}`}
@ -98,23 +110,23 @@ const McpView = ({ onDone }: McpViewProps) => {
)}
{/* Edit Settings Buttons */}
<div className="mt-[10px] w-full flex gap-[10px]">
<div style={{ marginTop: "10px", width: "100%", display: "flex", gap: "10px" }}>
<Button
variant="secondary"
className="flex-1"
style={{ flex: 1 }}
onClick={() => {
vscode.postMessage({ type: "openMcpSettings" })
}}>
<span className="codicon codicon-edit mr-[6px]"></span>
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
{t("mcp:editGlobalMCP")}
</Button>
<Button
variant="secondary"
className="flex-1"
style={{ flex: 1 }}
onClick={() => {
vscode.postMessage({ type: "openProjectMcpSettings" })
}}>
<span className="codicon codicon-edit mr-[6px]"></span>
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
{t("mcp:editProjectMCP")}
</Button>
</div>
@ -145,6 +157,17 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
{ value: 3600, label: t("mcp:networkTimeout.options.60minutes") },
]
const getStatusColor = () => {
switch (server.status) {
case "connected":
return "var(--vscode-testing-iconPassed)"
case "connecting":
return "var(--vscode-charts-yellow)"
case "disconnected":
return "var(--vscode-testing-iconFailed)"
}
}
const handleRowClick = () => {
if (server.status === "connected") {
setIsExpanded(!isExpanded)
@ -180,48 +203,74 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
}
return (
<div className="mb-[10px]">
<div style={{ marginBottom: "10px" }}>
<div
className={cn(
"flex items-center p-[8px] bg-vscode-textCodeBlock-background",
server.status === "connected" ? "cursor-pointer" : "cursor-default",
isExpanded || server.status === "connected" ? "rounded-[4px]" : "rounded-t-[4px]",
server.disabled ? "opacity-60" : "opacity-100",
)}
style={{
display: "flex",
alignItems: "center",
padding: "8px",
background: "var(--vscode-textCodeBlock-background)",
cursor: server.status === "connected" ? "pointer" : "default",
borderRadius: isExpanded || server.status === "connected" ? "4px" : "4px 4px 0 0",
opacity: server.disabled ? 0.6 : 1,
}}
onClick={handleRowClick}>
{server.status === "connected" && (
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} mr-[8px]`} />
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{ marginRight: "8px" }}
/>
)}
<span className="flex-1">
<span style={{ flex: 1 }}>
{server.name}
{server.source && (
<span className="ml-[8px] px-[6px] py-[1px] text-[11px] rounded-[4px] bg-vscode-badge-background text-vscode-badge-foreground">
<span
style={{
marginLeft: "8px",
padding: "1px 6px",
fontSize: "11px",
borderRadius: "4px",
background: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
}}>
{server.source}
</span>
)}
</span>
<div className="flex items-center mr-[8px]" onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" onClick={() => setShowDeleteConfirm(true)} className="mr-[8px]">
<span className="codicon codicon-trash text-[14px]"></span>
<div
style={{ display: "flex", alignItems: "center", marginRight: "8px" }}
onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="icon"
onClick={() => setShowDeleteConfirm(true)}
style={{ marginRight: "8px" }}>
<span className="codicon codicon-trash" style={{ fontSize: "14px" }}></span>
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleRestart}
disabled={server.status === "connecting"}
className="mr-[8px]">
<span className="codicon codicon-refresh text-[14px]"></span>
style={{ marginRight: "8px" }}>
<span className="codicon codicon-refresh" style={{ fontSize: "14px" }}></span>
</Button>
<div
role="switch"
aria-checked={!server.disabled}
tabIndex={0}
className={cn(
"w-[20px] h-[10px] rounded-[5px] relative cursor-pointer transition-colors duration-200",
server.disabled
? "bg-vscode-titleBar-inactiveForeground opacity-40"
: "bg-vscode-button-background opacity-80",
)}
style={{
width: "20px",
height: "10px",
backgroundColor: server.disabled
? "var(--vscode-titleBar-inactiveForeground)"
: "var(--vscode-button-background)",
borderRadius: "5px",
position: "relative",
cursor: "pointer",
transition: "background-color 0.2s",
opacity: server.disabled ? 0.4 : 0.8,
}}
onClick={() => {
vscode.postMessage({
type: "toggleMcpServer",
@ -242,26 +291,40 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
}
}}>
<div
className={cn(
"w-[6px] h-[6px] bg-vscode-titleBar-activeForeground rounded-full absolute top-[2px] transition-all duration-200",
server.disabled ? "left-[2px]" : "left-[12px]",
)}
style={{
width: "6px",
height: "6px",
backgroundColor: "var(--vscode-titleBar-activeForeground)",
borderRadius: "50%",
position: "absolute",
top: "2px",
left: server.disabled ? "2px" : "12px",
transition: "left 0.2s",
}}
/>
</div>
</div>
<div
className={cn("w-[8px] h-[8px] rounded-full ml-[8px]", {
"bg-vscode-testing-iconPassed": server.status === "connected",
"bg-vscode-charts-yellow": server.status === "connecting",
"bg-vscode-testing-iconFailed": server.status === "disconnected",
})}
style={{
width: "8px",
height: "8px",
borderRadius: "50%",
background: getStatusColor(),
marginLeft: "8px",
}}
/>
</div>
{server.status === "connected" ? (
isExpanded && (
<div className="bg-vscode-textCodeBlock-background px-[10px] pb-[10px] text-[13px] rounded-b-[4px]">
<VSCodePanels className="mb-[10px]">
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
padding: "0 10px 10px 10px",
fontSize: "13px",
borderRadius: "0 0 4px 4px",
}}>
<VSCodePanels style={{ marginBottom: "10px" }}>
<VSCodePanelTab id="tools">
{t("mcp:tabs.tools")} ({server.tools?.length || 0})
</VSCodePanelTab>
@ -275,7 +338,8 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
<VSCodePanelView id="tools-view">
{server.tools && server.tools.length > 0 ? (
<div className="flex flex-col gap-[8px] w-full">
<div
style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
{server.tools.map((tool) => (
<McpToolRow
key={`${tool.name}-${server.name}-${server.source || "global"}`}
@ -287,7 +351,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
))}
</div>
) : (
<div className="py-[10px] text-vscode-descriptionForeground">
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
{t("mcp:emptyState.noTools")}
</div>
)}
@ -296,7 +360,8 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
<VSCodePanelView id="resources-view">
{(server.resources && server.resources.length > 0) ||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
<div className="flex flex-col gap-[8px] w-full">
<div
style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
{[...(server.resourceTemplates || []), ...(server.resources || [])].map(
(item) => (
<McpResourceRow
@ -307,7 +372,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
)}
</div>
) : (
<div className="py-[10px] text-vscode-descriptionForeground">
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
{t("mcp:emptyState.noResources")}
</div>
)}
@ -315,7 +380,8 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
<VSCodePanelView id="errors-view">
{server.errorHistory && server.errorHistory.length > 0 ? (
<div className="flex flex-col gap-[8px] w-full">
<div
style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
{[...server.errorHistory]
.sort((a, b) => b.timestamp - a.timestamp)
.map((error, index) => (
@ -323,7 +389,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
))}
</div>
) : (
<div className="py-[10px] text-vscode-descriptionForeground">
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
{t("mcp:emptyState.noErrors")}
</div>
)}
@ -331,13 +397,28 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
</VSCodePanels>
{/* Network Timeout */}
<div className="px-[7px] py-[10px]">
<div className="flex items-center gap-[10px] mb-[8px]">
<div style={{ padding: "10px 7px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
marginBottom: "8px",
}}>
<span>{t("mcp:networkTimeout.label")}</span>
<select
value={timeoutValue}
onChange={handleTimeoutChange}
className="flex-1 p-[4px] bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded-[2px] outline-none cursor-pointer">
style={{
flex: 1,
padding: "4px",
background: "var(--vscode-dropdown-background)",
color: "var(--vscode-dropdown-foreground)",
border: "1px solid var(--vscode-dropdown-border)",
borderRadius: "2px",
outline: "none",
cursor: "pointer",
}}>
{timeoutOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
@ -345,15 +426,33 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
))}
</select>
</div>
<span className="text-xs text-vscode-descriptionForeground block">
<span
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
display: "block",
}}>
{t("mcp:networkTimeout.description")}
</span>
</div>
</div>
)
) : (
<div className="text-[13px] bg-vscode-textCodeBlock-background rounded-b-[4px] w-full">
<div className="text-vscode-testing-iconFailed mb-[8px] px-[10px] overflow-wrap-break-word break-words">
<div
style={{
fontSize: "13px",
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "0 0 4px 4px",
width: "100%",
}}>
<div
style={{
color: "var(--vscode-testing-iconFailed)",
marginBottom: "8px",
padding: "0 10px",
overflowWrap: "break-word",
wordBreak: "break-word",
}}>
{server.error &&
server.error.split("\n").map((item, index) => (
<React.Fragment key={index}>
@ -366,7 +465,7 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
appearance="secondary"
onClick={handleRestart}
disabled={server.status === "connecting"}
className="w-[calc(100%-20px)] mx-[10px] mb-[10px]">
style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
</VSCodeButton>
</div>

View file

@ -434,14 +434,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
return (
<Tab>
<TabHeader className="flex justify-between items-center">
<h3 className="text-foreground m-0">{t("prompts:title")}</h3>
<h3 className="text-vscode-foreground m-0">{t("prompts:title")}</h3>
<Button onClick={onDone}>{t("prompts:done")}</Button>
</TabHeader>
<TabContent>
<div>
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
<h3 className="text-foreground m-0">{t("prompts:modes.title")}</h3>
<h3 className="text-vscode-foreground m-0">{t("prompts:modes.title")}</h3>
<div className="flex gap-2">
<Button
variant="ghost"
@ -471,9 +471,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
className="absolute top-full right-0 w-[200px] mt-1 bg-background border border-border rounded shadow-md z-[1000]">
className="absolute top-full right-0 w-[200px] mt-1 bg-vscode-editor-background border border-vscode-input-border rounded shadow-md z-[1000]">
<div
className="p-2 cursor-pointer text-foreground text-sm"
className="p-2 cursor-pointer text-vscode-foreground text-sm"
onMouseDown={(e) => {
e.preventDefault() // Prevent blur
vscode.postMessage({
@ -485,7 +485,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{t("prompts:modes.editGlobalModes")}
</div>
<div
className="p-2 cursor-pointer text-foreground text-sm border-t border-border"
className="p-2 cursor-pointer text-vscode-foreground text-sm border-t border-vscode-input-border"
onMouseDown={(e) => {
e.preventDefault() // Prevent blur
vscode.postMessage({
@ -507,7 +507,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
<div className="text-sm text-muted-foreground mb-3">{t("prompts:modes.createModeHelpText")}</div>
<div className="text-sm text-vscode-descriptionForeground mb-3">
{t("prompts:modes.createModeHelpText")}
</div>
<div className="flex items-center gap-1 mb-3">
<Popover open={open} onOpenChange={onOpenChange}>
@ -639,7 +641,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</Button>
)}
</div>
<div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:roleDefinition.description")}
</div>
<Textarea
@ -700,7 +702,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
))}
</SelectContent>
</Select>
<div className="text-xs mt-1.5 text-muted-foreground">
<div className="text-xs mt-1.5 text-vscode-descriptionForeground">
{t("prompts:apiConfiguration.select")}
</div>
</div>
@ -726,7 +728,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
</div>
{!findModeBySlug(visualMode, customModes) && (
<div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:tools.builtInModesText")}
</div>
)}
@ -748,7 +750,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
disabled={!isCustomMode}>
{t(`prompts:tools.toolNames.${group}`)}
{group === "edit" && (
<div className="text-xs text-muted-foreground mt-0.5">
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
{t("prompts:tools.allowedFiles")}{" "}
{(() => {
const currentMode = getCurrentMode()
@ -771,7 +773,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
})}
</div>
) : (
<div className="text-sm text-foreground mb-2 leading-relaxed">
<div className="text-sm text-vscode-foreground mb-2 leading-relaxed">
{(() => {
const currentMode = getCurrentMode()
const enabledGroups = currentMode?.groups || []
@ -819,7 +821,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</Button>
)}
</div>
<div className="text-[13px] text-muted-foreground mb-2">
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
{t("prompts:customInstructions.description", {
modeName: getCurrentMode()?.name || "Code",
})}
@ -859,7 +861,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
className="w-full resize-y"
data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`}
/>
<div className="text-xs text-muted-foreground mt-1.5">
<div className="text-xs text-vscode-descriptionForeground mt-1.5">
<Trans
i18nKey="prompts:customInstructions.loadFromFile"
values={{
@ -892,7 +894,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
<div className="pb-4 border-b border-border">
<div className="pb-4 border-b border-vscode-input-border">
<div className="flex gap-2">
<Button
variant="default"
@ -930,7 +932,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div className="mt-4">
<button
onClick={() => setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
className="flex items-center text-xs text-foreground hover:text-vscode-textLink-foreground focus:outline-none"
className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
aria-expanded={isSystemPromptDisclosureOpen}>
<span
className={`codicon codicon-${isSystemPromptDisclosureOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
@ -938,7 +940,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</button>
{isSystemPromptDisclosureOpen && (
<div className="text-xs text-muted-foreground mt-2 ml-5">
<div className="text-xs text-vscode-descriptionForeground mt-2 ml-5">
<Trans
i18nKey="prompts:advancedSystemPrompt.description"
values={{
@ -970,10 +972,10 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
<div className="pb-5 border-b border-border">
<h3 className="text-foreground mb-3">{t("prompts:globalCustomInstructions.title")}</h3>
<div className="pb-5 border-b border-vscode-input-border">
<h3 className="text-vscode-foreground mb-3">{t("prompts:globalCustomInstructions.title")}</h3>
<div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:globalCustomInstructions.description", {
language: i18next.language,
})}
@ -994,7 +996,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
className="w-full resize-y"
data-testid="global-custom-instructions-textarea"
/>
<div className="text-xs text-muted-foreground mt-1.5">
<div className="text-xs text-vscode-descriptionForeground mt-1.5">
<Trans
i18nKey="prompts:globalCustomInstructions.loadFromFile"
components={{
@ -1018,8 +1020,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
<div className="mt-5 pb-[3.75rem] border-b border-border">
<h3 className="text-foreground mb-3">{t("prompts:supportPrompts.title")}</h3>
<div className="mt-5 pb-15 border-b border-vscode-input-border">
<h3 className="text-vscode-foreground mb-3">{t("prompts:supportPrompts.title")}</h3>
<div className="flex gap-4 items-center flex-wrap py-1">
<Select
value={activeSupportOption}
@ -1038,7 +1040,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
{/* Support prompt description */}
<div className="text-[13px] text-muted-foreground my-2 mb-4">
<div className="text-[13px] text-vscode-descriptionForeground my-2 mb-4">
{t(`prompts:supportPrompts.types.${activeSupportOption}.description`)}
</div>
@ -1072,13 +1074,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{activeSupportOption === "ENHANCE" && (
<>
<div>
<div className="text-foreground text-[13px] mb-5 mt-1.5"></div>
<div className="text-vscode-foreground text-[13px] mb-5 mt-1.5"></div>
<div className="mb-3">
<div className="mb-2">
<div className="font-bold mb-1">
{t("prompts:supportPrompts.enhance.apiConfiguration")}
</div>
<div className="text-[13px] text-muted-foreground">
<div className="text-[13px] text-vscode-descriptionForeground">
{t("prompts:supportPrompts.enhance.apiConfigDescription")}
</div>
</div>
@ -1141,7 +1143,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{isCreateModeDialogOpen && (
<div className="fixed inset-0 flex justify-end bg-black/50 z-[1000]">
<div className="w-[calc(100vw-100px)] h-full bg-background shadow-md flex flex-col relative">
<div className="w-[calc(100vw-100px)] h-full bg-vscode-editor-background shadow-md flex flex-col relative">
<div className="flex-1 p-5 overflow-y-auto min-h-0">
<Button
variant="ghost"
@ -1161,7 +1163,9 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}}
className="w-full"
/>
{nameError && <div className="text-xs text-destructive mt-1">{nameError}</div>}
{nameError && (
<div className="text-xs text-vscode-errorForeground mt-1">{nameError}</div>
)}
</div>
<div className="mb-4">
<div className="font-bold mb-1">{t("prompts:createModeDialog.slug.label")}</div>
@ -1173,14 +1177,16 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}}
className="w-full"
/>
<div className="text-xs text-muted-foreground mt-1">
<div className="text-xs text-vscode-descriptionForeground mt-1">
{t("prompts:createModeDialog.slug.description")}
</div>
{slugError && <div className="text-xs text-destructive mt-1">{slugError}</div>}
{slugError && (
<div className="text-xs text-vscode-errorForeground mt-1">{slugError}</div>
)}
</div>
<div className="mb-4">
<div className="font-bold mb-1">{t("prompts:createModeDialog.saveLocation.label")}</div>
<div className="text-sm text-muted-foreground mb-2">
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("prompts:createModeDialog.saveLocation.description")}
</div>
<VSCodeRadioGroup
@ -1192,24 +1198,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}}>
<VSCodeRadio value="global">
{t("prompts:createModeDialog.saveLocation.global.label")}
<div className="text-xs text-muted-foreground mt-0.5">
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
{t("prompts:createModeDialog.saveLocation.global.description")}
</div>
</VSCodeRadio>
<VSCodeRadio value="project">
{t("prompts:createModeDialog.saveLocation.project.label")}
<div className="text-xs text-muted-foreground mt-0.5">
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
{t("prompts:createModeDialog.saveLocation.project.description")}
</div>
</VSCodeRadio>
</VSCodeRadioGroup>
</div>
<div className="mb-4">
<div className="font-bold mb-1">
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
{t("prompts:createModeDialog.roleDefinition.label")}
</div>
<div className="text-[13px] text-muted-foreground mb-2">
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
{t("prompts:createModeDialog.roleDefinition.description")}
</div>
<Textarea
@ -1221,12 +1232,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
className="w-full resize-y"
/>
{roleDefinitionError && (
<div className="text-xs text-destructive mt-1">{roleDefinitionError}</div>
<div className="text-xs text-vscode-errorForeground mt-1">
{roleDefinitionError}
</div>
)}
</div>
<div className="mb-4">
<div className="font-bold mb-1">{t("prompts:createModeDialog.tools.label")}</div>
<div className="text-[13px] text-muted-foreground mb-2">
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
{t("prompts:createModeDialog.tools.description")}
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
@ -1250,13 +1263,15 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeCheckbox>
))}
</div>
{groupsError && <div className="text-xs text-destructive mt-1">{groupsError}</div>}
{groupsError && (
<div className="text-xs text-vscode-errorForeground mt-1">{groupsError}</div>
)}
</div>
<div className="mb-4">
<div className="font-bold mb-1">
{t("prompts:createModeDialog.customInstructions.label")}
</div>
<div className="text-[13px] text-muted-foreground mb-2">
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
{t("prompts:createModeDialog.customInstructions.description")}
</div>
<Textarea
@ -1269,7 +1284,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
/>
</div>
</div>
<div className="flex justify-end p-3 px-5 gap-2 border-t border-vscode-editor-lineHighlightBorder bg-background">
<div className="flex justify-end p-3 px-5 gap-2 border-t border-vscode-editor-lineHighlightBorder bg-vscode-editor-background">
<Button variant="secondary" onClick={() => setIsCreateModeDialogOpen(false)}>
{t("prompts:createModeDialog.buttons.cancel")}
</Button>
@ -1283,7 +1298,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{isDialogOpen && (
<div className="fixed inset-0 flex justify-end bg-black/50 z-[1000]">
<div className="w-[calc(100vw-100px)] h-full bg-background shadow-md flex flex-col relative">
<div className="w-[calc(100vw-100px)] h-full bg-vscode-editor-background shadow-md flex flex-col relative">
<div className="flex-1 p-5 overflow-y-auto min-h-0">
<Button
variant="ghost"
@ -1298,11 +1313,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
modeName: getCurrentMode()?.name || "Code",
})}
</h2>
<pre className="p-2 whitespace-pre-wrap break-words font-mono text-base text-foreground bg-background border border-vscode-editor-lineHighlightBorder rounded overflow-y-auto">
<pre className="p-2 whitespace-pre-wrap break-words font-mono text-vscode-editor-font-size text-vscode-editor-foreground bg-vscode-editor-background border border-vscode-editor-lineHighlightBorder rounded overflow-y-auto">
{selectedPromptContent}
</pre>
</div>
<div className="flex justify-end p-3 px-5 border-t border-vscode-editor-lineHighlightBorder bg-background">
<div className="flex justify-end p-3 px-5 border-t border-vscode-editor-lineHighlightBorder bg-vscode-editor-background">
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
{t("prompts:createModeDialog.close")}
</Button>

View file

@ -372,7 +372,7 @@ const ApiConfigManager = ({
}}
placeholder={t("settings:providers.enterProfileName")}
data-testid="new-profile-input"
className="w-full"
style={{ width: "100%" }}
onKeyDown={(e: unknown) => {
const event = e as { key: string }
if (event.key === "Enter" && newProfileName.trim()) {

View file

@ -2,7 +2,6 @@ import { VSCodeButton, VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-u
import { SquareMousePointer } from "lucide-react"
import { HTMLAttributes, useEffect, useMemo, useState } from "react"
import { cn } from "@/lib/utils"
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@/utils/vscode"
@ -189,7 +188,7 @@ export const BrowserSettings = ({
setCachedStateField("remoteBrowserHost", e.target.value || undefined)
}
placeholder={t("settings:browser.remote.urlPlaceholder")}
className="grow"
style={{ flexGrow: 1 }}
/>
<VSCodeButton disabled={testingConnection} onClick={testConnection}>
{testingConnection || discovering
@ -199,12 +198,11 @@ export const BrowserSettings = ({
</div>
{testResult && (
<div
className={cn(
"p-2 rounded-xs text-sm",
className={`p-2 rounded-xs text-sm ${
testResult.success
? "bg-green-800/20 text-green-400"
: "bg-red-800/20 text-red-400",
)}>
: "bg-red-800/20 text-red-400"
}`}>
{testResult.text}
</div>
)}

View file

@ -1,32 +1,11 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { memo, useEffect, useRef, useState, FC, PropsWithChildren } from "react" // Added React, FC, PropsWithChildren
import { memo, useEffect, useRef, useState } from "react"
import { useRemark } from "react-remark"
import { cn } from "@/lib/utils"
import { Collapsible, CollapsibleTrigger } from "@/components/ui"
// Removed import { StyledMarkdown } from "./styles"
// Moved StyledMarkdown component definition here
interface StyledMarkdownProps extends React.HTMLAttributes<HTMLDivElement> {}
const StyledMarkdown: FC<PropsWithChildren<StyledMarkdownProps>> = ({ className, children, ...props }) => {
return (
<div
className={cn(
"font-vscode-font-family text-xs text-vscode-descriptionForeground",
"[&_p]:leading-tight [&_p]:m-0 [&_p]:whitespace-pre-wrap",
"[&_li]:leading-tight [&_li]:m-0",
"[&_ol]:leading-tight [&_ol]:m-0 [&_ol]:pl-[1.5em] [&_ol]:ml-0",
"[&_ul]:leading-tight [&_ul]:m-0 [&_ul]:pl-[1.5em] [&_ul]:ml-0",
"[&_a]:no-underline hover:[&_a]:underline",
className,
)}
{...props}>
{children}
</div>
)
}
import { StyledMarkdown } from "./styles"
export const ModelDescriptionMarkdown = memo(
({

View file

@ -414,7 +414,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<div className="flex gap-2">
<Button
variant={isSettingValid ? "default" : "secondary"}
className={cn(!isSettingValid && "!border-vscode-errorForeground")}
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
title={
!isSettingValid
? errorMessage

View file

@ -109,8 +109,9 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
<div className="flex items-center gap-1">
<span>{t("settings:providers.enablePromptCaching")}</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.enablePromptCachingTitle")}
style={{ fontSize: "12px" }}
/>
</div>
</Checkbox>

View file

@ -45,7 +45,7 @@ export const Glama = ({ apiConfiguration, setApiConfigurationField, routerModels
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.glamaApiKey && (
<VSCodeButtonLink href={getGlamaAuthUrl(uriScheme)} className="w-full" appearance="primary">
<VSCodeButtonLink href={getGlamaAuthUrl(uriScheme)} style={{ width: "100%" }} appearance="primary">
{t("settings:providers.getGlamaApiKey")}
</VSCodeButtonLink>
)}

View file

@ -119,7 +119,13 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
))}
</VSCodeRadioGroup>
{lmStudioModels.length === 0 && (
<div className="text-sm rounded-xs p-2 bg-vscode-inputValidation-infoBackground border border-vscode-inputValidation-infoBorder text-vscode-inputValidation-infoForeground">
<div
className="text-sm rounded-xs p-2"
style={{
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
border: "1px solid var(--vscode-inputValidation-infoBorder)",
color: "var(--vscode-inputValidation-infoForeground)",
}}>
{t("settings:providers.lmStudio.noModelsFound")}
</div>
)}

View file

@ -7,7 +7,6 @@ import { ModelInfo, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
import { cn } from "@/lib/utils" // Added cn import
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"
@ -252,16 +251,17 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
""
}
type="text"
className={cn(
"w-full",
"border", // Added base border class
(() => {
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens
// Use `value === undefined || value === null` for a more robust check if 0 is a valid input but should not trigger default border
if (value === undefined || value === null) return "border-vscode-input-border"
return value > 0 ? "border-vscode-charts-green" : "border-vscode-errorForeground"
if (!value) {
return "var(--vscode-input-border)"
}
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
)}
}}
title={t("settings:providers.customModel.maxTokens.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = parseInt((e.target as HTMLInputElement).value)
@ -272,8 +272,7 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
}
})}
placeholder={t("settings:placeholders.numbers.maxTokens")}
/* className="w-full" */
>
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.maxTokens.label")}
</label>
@ -291,6 +290,17 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
if (!value) {
return "var(--vscode-input-border)"
}
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
title={t("settings:providers.customModel.contextWindow.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
@ -302,14 +312,7 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
}
})}
placeholder={t("settings:placeholders.numbers.contextWindow")}
className={cn(
"w-full border", // Added base 'border' class
(() => {
const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
if (value === undefined || value === null) return "border-vscode-input-border" // Default border if no value
return value > 0 ? "border-vscode-charts-green" : "border-vscode-errorForeground"
})(),
)}>
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.contextWindow.label")}
</label>
@ -337,8 +340,9 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.imageSupport.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
@ -359,8 +363,9 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
<span className="font-medium">{t("settings:providers.customModel.computerUse.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.computerUse.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
@ -381,8 +386,9 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
<span className="font-medium">{t("settings:providers.customModel.promptCache.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.promptCache.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
@ -398,6 +404,17 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
@ -408,21 +425,15 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
}
})}
placeholder={t("settings:placeholders.numbers.inputPrice")}
className={cn(
"w-full border", // Added base 'border' class
(() => {
const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
if (value === undefined || value === null) return "border-vscode-input-border"
return value >= 0 ? "border-vscode-charts-green" : "border-vscode-errorForeground"
})(),
)}>
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.input.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.input.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
@ -436,6 +447,17 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
@ -446,21 +468,15 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
}
})}
placeholder={t("settings:placeholders.numbers.outputPrice")}
className={cn(
"w-full border", // Added base 'border' class
(() => {
const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
if (value === undefined || value === null) return "border-vscode-input-border"
return value >= 0 ? "border-vscode-charts-green" : "border-vscode-errorForeground"
})(),
)}>
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.output.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.output.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
@ -472,16 +488,19 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
<VSCodeTextField
value={apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice?.toString() ?? "0"}
type="text"
className={cn(
"w-full border", // Added base 'border' class
(() => {
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice
if (value === undefined || value === null) return "border-vscode-input-border"
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0
? "border-vscode-charts-green"
: "border-vscode-errorForeground"
? "var(--vscode-charts-green)"
: "var(--vscode-errorForeground)"
})(),
)}
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
@ -491,14 +510,16 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
cacheReadsPrice: isNaN(parsed) ? 0 : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.inputPrice")}>
placeholder={t("settings:placeholders.numbers.inputPrice")}
className="w-full">
<div className="flex items-center gap-1">
<span className="font-medium">
{t("settings:providers.customModel.pricing.cacheReads.label")}
</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheReads.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
@ -507,6 +528,19 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
<VSCodeTextField
value={apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice?.toString() ?? "0"}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0
? "var(--vscode-charts-green)"
: "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
@ -517,23 +551,15 @@ export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }:
}
})}
placeholder={t("settings:placeholders.numbers.cacheWritePrice")}
className={cn(
"w-full border",
(() => {
const value = apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice
if (value === undefined || value === null) return "border-vscode-input-border"
return value >= 0
? "border-vscode-charts-green"
: "border-vscode-errorForeground"
})(),
)}>
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.cacheWrites.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground text-xs"
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheWrites.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>

View file

@ -82,7 +82,7 @@ export const OpenRouter = ({
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} className="w-full" appearance="primary">
<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} style={{ width: "100%" }} appearance="primary">
{t("settings:providers.getOpenRouterApiKey")}
</VSCodeButtonLink>
)}

View file

@ -59,7 +59,10 @@ export const Requesty = ({
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.requestyApiKey && (
<VSCodeButtonLink href="https://app.requesty.ai/api-keys" className="w-full" appearance="primary">
<VSCodeButtonLink
href="https://app.requesty.ai/api-keys"
style={{ width: "100%" }}
appearance="primary">
{t("settings:providers.getRequestyApiKey")}
</VSCodeButtonLink>
)}

View file

@ -0,0 +1,47 @@
import styled from "styled-components"
// Keep StyledMarkdown as it's used by ModelDescriptionMarkdown.tsx
export const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 12px;
color: var(--vscode-descriptionForeground);
p,
li,
ol,
ul {
line-height: 1.25;
margin: 0;
}
ol,
ul {
padding-left: 1.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
text-decoration: none;
}
a {
&:hover {
text-decoration: underline;
}
}
`

View file

@ -40,7 +40,7 @@ function DialogContent({ className, children, ...props }: React.ComponentProps<t
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-vscode-editor-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}>
@ -78,7 +78,7 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold my-0", className)}
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
@ -88,7 +88,7 @@ function DialogDescription({ className, ...props }: React.ComponentProps<typeof
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm my-0", className)}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)

View file

@ -1,3 +1,3 @@
export const Blockquote = ({ children }: { children: React.ReactNode }) => {
return <div className="border-l-[3px] border-accent italic pl-2 py-2 mb-2">{children}</div>
return <div className="border-l-3 border-accent italic pl-2 py-2 mb-2">{children}</div>
}

View file

@ -60,7 +60,11 @@ export const CodeBlock: FC<CodeBlockProps> = memo(({ language, value, className,
size="icon"
className="absolute top-1 right-1 cursor-pointer bg-black/10"
onClick={onCopy}>
{isCopied ? <CheckIcon className="w-3 h-3" /> : <CopyIcon className="w-3 h-3" />}
{isCopied ? (
<CheckIcon style={{ width: 12, height: 12 }} />
) : (
<CopyIcon style={{ width: 12, height: 12 }} />
)}
</Button>
</div>
)

View file

@ -123,51 +123,6 @@
--color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground);
--color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
--color-vscode-descriptionForeground-transparent-30: color-mix(
in srgb,
var(--vscode-descriptionForeground) 30%,
transparent
);
--color-vscode-testing-iconPassed: var(--vscode-testing-iconPassed);
--color-vscode-testing-iconFailed: var(--vscode-testing-iconFailed);
--color-vscode-titleBar-inactiveForeground: var(--vscode-titleBar-inactiveForeground);
--color-vscode-titleBar-activeForeground: var(--vscode-titleBar-activeForeground);
--color-vscode-editor-lineHighlightBorder: var(--vscode-editor-lineHighlightBorder);
--color-vscode-titleBar-inactiveForeground-20: color-mix(
in srgb,
var(--vscode-titleBar-inactiveForeground) 20%,
transparent
);
/* Custom color for CodeBlock background with fallbacks */
--color-vscode-code-block-background: var(
--vscode-editor-background,
var(--vscode-sideBar-background, rgb(30 30 30))
);
/* Custom colors for Slider border */
--color-vscode-slider-border-light: #767676;
--color-vscode-slider-border-dark: #858585;
/* Fallback colors for Highlight.js (Light theme) */
--color-vscode-fallback-light-comment: #008000;
--color-vscode-fallback-light-doctag: #0000ff;
--color-vscode-fallback-light-keyword: #0000ff;
--color-vscode-fallback-light-title-class: #001080;
--color-vscode-fallback-light-title-function: #795e26;
--color-vscode-fallback-light-number: #098658;
--color-vscode-fallback-light-regexp: #a31515;
--color-vscode-fallback-light-string: #a31515;
/* Fallback colors for Highlight.js (Dark theme) */
--color-vscode-fallback-dark-comment: #6a9955;
--color-vscode-fallback-dark-doctag: #569cd6;
--color-vscode-fallback-dark-keyword: #569cd6;
--color-vscode-fallback-dark-title-class: #9cdcfe;
--color-vscode-fallback-dark-title-function: #dcdcaa;
--color-vscode-fallback-dark-number: #b5cea8;
--color-vscode-fallback-dark-regexp: #ce9178;
--color-vscode-fallback-dark-string: #ce9178;
}
@layer base {
@ -229,11 +184,6 @@
.history-item-highlight {
@apply underline;
}
/* Shiki code block inner code transparency */
.shiki > code {
background-color: transparent !important;
}
}
/* Form Element Focus States */
@ -463,16 +413,3 @@ input[cmdk-input]:focus {
.codicon[class*="codicon-"] {
text-rendering: geometricPrecision !important;
}
/**
* Animations
*/
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}