diff --git a/src/api/providers/gemini-cli.ts b/src/api/providers/gemini-cli.ts index 320a1b3b42..f6a80cf2bd 100644 --- a/src/api/providers/gemini-cli.ts +++ b/src/api/providers/gemini-cli.ts @@ -405,8 +405,10 @@ export class GeminiCliHandler extends BaseProvider implements SingleCompletionHa data: JSON.stringify(requestBody), }) - // Extract text from response - const responseData = response.data as any + // Extract text from response, handling both direct and nested response structures + const rawData = response.data as any + const responseData = rawData.response || rawData + if (responseData.candidates && responseData.candidates.length > 0) { const candidate = responseData.candidates[0] if (candidate.content && candidate.content.parts) { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1d72827028..c758f74418 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -923,6 +923,18 @@ export const webviewMessageHandler = async ( } break } + case "addMcpServer": { + if (message.text && message.source) { + try { + await provider.getMcpHub()?.addServer(message.text, message.source as "global" | "project") + await provider.postStateToWebview() + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Failed to add MCP server: ${errorMessage}`) + } + } + break + } case "restartMcpServer": { try { await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project") diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 26d6a6318d..0267f43e1d 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -1484,6 +1484,36 @@ export class McpHub { await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)) } + public async addServer(serverConfigText: string, source: "global" | "project"): Promise { + try { + // Parse the server configuration from JSON text + let serverConfig: any + try { + serverConfig = JSON.parse(serverConfigText) + } catch (parseError) { + throw new Error( + `Invalid JSON configuration: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + ) + } + + // Validate that we have a server name + if (!serverConfig.name) { + throw new Error("Server configuration must include a 'name' field") + } + + const serverName = serverConfig.name + + // Remove the name from the config since it's used as a key + const { name, ...serverConfigWithoutName } = serverConfig + + // Use updateServerConnections to add the new server + await this.updateServerConnections({ [serverName]: serverConfigWithoutName }, source) + } catch (error) { + this.showErrorMessage(`Failed to add MCP server`, error) + throw error + } + } + public async updateServerTimeout( serverName: string, timeout: number, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index dee2aba5fb..9094d46e77 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -112,6 +112,7 @@ export interface WebviewMessage { | "remoteBrowserHost" | "openMcpSettings" | "openProjectMcpSettings" + | "addMcpServer" | "restartMcpServer" | "refreshAllMcpServers" | "toggleToolAlwaysAllow" diff --git a/src/utils/single-completion-handler.ts b/src/utils/single-completion-handler.ts index 5798905035..a36d1f3f22 100644 --- a/src/utils/single-completion-handler.ts +++ b/src/utils/single-completion-handler.ts @@ -1,41 +1,49 @@ import type { ProviderSettings } from "@roo-code/types" -import { buildApiHandler, SingleCompletionHandler, ApiHandler } from "../api" //kilocode_change +import { buildApiHandler, SingleCompletionHandler, ApiHandler } from "../api" /** * Enhances a prompt using the configured API without creating a full Cline instance or task history. * This is a lightweight alternative that only uses the API's completion functionality. */ export async function singleCompletionHandler(apiConfiguration: ProviderSettings, promptText: string): Promise { - if (!promptText) { - throw new Error("No prompt text provided") - } - if (!apiConfiguration || !apiConfiguration.apiProvider) { - throw new Error("No valid API configuration provided") - } + if (!promptText) { + throw new Error("No prompt text provided") + } + if (!apiConfiguration || !apiConfiguration.apiProvider) { + throw new Error("No valid API configuration provided") + } - const handler = buildApiHandler(apiConfiguration) + const handler = buildApiHandler(apiConfiguration) - // Check if handler supports single completions - if (!("completePrompt" in handler)) { - // kilocode_change start - stream responses for handlers without completePrompt - // throw new Error("The selected API provider does not support prompt enhancement") - return await streamResponseFromHandler(handler, promptText) - // kilocode_change end - } + // kilocode_change start + // Force gemini-cli to use completePrompt + if (apiConfiguration.apiProvider === "gemini-cli") { + if ("completePrompt" in handler) { // Add check for safety + return (handler as SingleCompletionHandler).completePrompt(promptText) + } else { + throw new Error("Gemini-cli handler does not support completePrompt as expected.") + } + } + // kilocode_change end - return (handler as SingleCompletionHandler).completePrompt(promptText) + // Check if handler supports single completions + if ("completePrompt" in handler) { // If completePrompt exists, use it + return (handler as SingleCompletionHandler).completePrompt(promptText) + } else { // Otherwise, stream responses + return await streamResponseFromHandler(handler, promptText) + } } // kilocode_change start - Stream responses using createMessage async function streamResponseFromHandler(handler: ApiHandler, promptText: string): Promise { - const stream = handler.createMessage("", [{ role: "user", content: [{ type: "text", text: promptText }] }]) + const stream = handler.createMessage("", [{ role: "user", content: [{ type: "text", text: promptText }] }]) - let response: string = "" - for await (const chunk of stream) { - if (chunk.type === "text") { - response += chunk.text - } - } - return response + let response: string = "" + for await (const chunk of stream) { + if (chunk.type === "text") { + response += chunk.text + } + } + return response } // kilocode_change end - streamResponseFromHandler diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index 28492acd8b..eaccef2081 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -233,6 +233,7 @@ const CodeBlock = memo( const codeBlockRef = useRef(null) const preRef = useRef(null) const copyButtonWrapperRef = useRef(null) + // Copy button ref no longer needed since we're using onClick const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard() const { t } = useAppTranslation() const isMountedRef = useRef(true) @@ -647,38 +648,23 @@ const CodeBlock = memo( const [isSelecting, setIsSelecting] = useState(false) useEffect(() => { - if (!preRef.current) return - - const handleMouseDown = (e: MouseEvent) => { - // Only trigger if clicking the pre element directly - if (e.currentTarget === preRef.current) { - setIsSelecting(true) - } + const handleSelectionChange = () => { + const selection = window.getSelection() + const newIsSelecting = selection ? !selection.isCollapsed : false + console.log("Selection changed, isSelecting:", newIsSelecting) + setIsSelecting(newIsSelecting) } - const handleMouseUp = () => { - setIsSelecting(false) - } - - const preElement = preRef.current - preElement.addEventListener("mousedown", handleMouseDown) - document.addEventListener("mouseup", handleMouseUp) + document.addEventListener("selectionchange", handleSelectionChange) return () => { - preElement.removeEventListener("mousedown", handleMouseDown) - document.removeEventListener("mouseup", handleMouseUp) + document.removeEventListener("selectionchange", handleSelectionChange) } }, []) const handleCopy = useCallback( (e: React.MouseEvent) => { e.stopPropagation() - - // Check if code block is partially visible before allowing copy - const codeBlock = codeBlockRef.current - if (!codeBlock || codeBlock.getAttribute("data-partially-visible") !== "true") { - return - } const textToCopy = rawSource !== undefined ? rawSource : source || "" if (textToCopy) { copyWithFeedback(textToCopy, e) @@ -691,6 +677,7 @@ const CodeBlock = memo( return null } + console.log("Rendering CodeBlock, isSelecting:", isSelecting) return ( - + {showCopyFeedback ? : } diff --git a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx index f413745b61..2e531c19e1 100644 --- a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx @@ -84,10 +84,11 @@ vi.mock("../../../utils/highlighter", () => { }) // Mock clipboard utility +const mockCopyWithFeedback = vi.fn() vi.mock("../../../utils/clipboard", () => ({ useCopyToClipboard: () => ({ showCopyFeedback: false, - copyWithFeedback: vi.fn(), + copyWithFeedback: mockCopyWithFeedback, }), })) @@ -199,23 +200,18 @@ describe("CodeBlock", () => { it("handles copy functionality", async () => { const code = "const x = 1;" - const { container } = render() - // Simulate code block visibility - const codeBlock = container.querySelector("[data-partially-visible]") - if (codeBlock) { - codeBlock.setAttribute("data-partially-visible", "true") - } + render() - // Find the copy button by looking for the button containing the Copy icon - const buttons = screen.getAllByRole("button") - const copyButton = buttons.find((btn) => btn.querySelector("svg.lucide-copy")) + // Wait for the highlighter to finish and render the code + await screen.findByText(/\[dark-theme\]/, undefined, { timeout: 4000 }) - expect(copyButton).toBeTruthy() - if (copyButton) { - await act(async () => { - fireEvent.click(copyButton) - }) - } + const copyButton = screen.getByTestId("codeblock-copy-button") + expect(copyButton).toBeInTheDocument() + + fireEvent.click(copyButton) + + // Verify the copyWithFeedback mock was called + expect(mockCopyWithFeedback).toHaveBeenCalledWith(code, expect.anything()) }) }) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 186f00bbc1..bde2a44cd8 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -160,26 +160,15 @@ const McpView = ({ onDone, hideHeader = false }: McpViewProps) => { {t("mcp:refreshMCP")} - {/* kilocode_change - - - - */} + {/* kilocode_change start */}
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index c82518864a..9e5fdcc703 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -238,6 +238,13 @@ vi.mock("../providers/LiteLLM", () => ({ ), })) +vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: vi.fn(() => ({ + data: {}, + refetch: vi.fn(), + })), +})) + vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ useSelectedModel: vi.fn((apiConfiguration: ProviderSettings) => { if (apiConfiguration.apiModelId?.includes("thinking")) {