Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/api/providers/gemini-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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,
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface WebviewMessage {
| "remoteBrowserHost"
| "openMcpSettings"
| "openProjectMcpSettings"
| "addMcpServer"
| "restartMcpServer"
| "refreshAllMcpServers"
| "toggleToolAlwaysAllow"
Expand Down
56 changes: 32 additions & 24 deletions src/utils/single-completion-handler.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string> {
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
33 changes: 10 additions & 23 deletions webview-ui/src/components/common/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ const CodeBlock = memo(
const codeBlockRef = useRef<HTMLDivElement>(null)
const preRef = useRef<HTMLDivElement>(null)
const copyButtonWrapperRef = useRef<HTMLDivElement>(null)
// Copy button ref no longer needed since we're using onClick
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
const { t } = useAppTranslation()
const isMountedRef = useRef(true)
Expand Down Expand Up @@ -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)
Expand All @@ -691,6 +677,7 @@ const CodeBlock = memo(
return null
}

console.log("Rendering CodeBlock, isSelecting:", isSelecting)
return (
<CodeBlockContainer ref={codeBlockRef}>
<MemoizedStyledPre
Expand Down Expand Up @@ -795,7 +782,7 @@ const CodeBlock = memo(
</CodeBlockButton>
</StandardTooltip>
<StandardTooltip content={t("chat:codeblock.tooltips.copy_code")} side="top">
<CodeBlockButton onClick={handleCopy}>
<CodeBlockButton onClick={handleCopy} data-testid="codeblock-copy-button">
{showCopyFeedback ? <Check size={16} /> : <Copy size={16} />}
</CodeBlockButton>
</StandardTooltip>
Expand Down
28 changes: 12 additions & 16 deletions webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
}))

Expand Down Expand Up @@ -199,23 +200,18 @@ describe("CodeBlock", () => {

it("handles copy functionality", async () => {
const code = "const x = 1;"
const { container } = render(<CodeBlock source={code} language="typescript" />)

// Simulate code block visibility
const codeBlock = container.querySelector("[data-partially-visible]")
if (codeBlock) {
codeBlock.setAttribute("data-partially-visible", "true")
}
render(<CodeBlock source={code} language="typescript" />)

// 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())
})
})
29 changes: 9 additions & 20 deletions webview-ui/src/components/mcp/McpView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,26 +160,15 @@ const McpView = ({ onDone, hideHeader = false }: McpViewProps) => {
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }}></span>
{t("mcp:refreshMCP")}
</Button>
{/* kilocode_change
<StandardTooltip content={t("mcp:marketplace")}>
<Button
variant="secondary"
style={{ width: "100%" }}
onClick={() => {
window.postMessage(
{
type: "action",
action: "marketplaceButtonClicked",
values: { marketplaceTab: "mcp" },
},
"*",
)
}}>
<span className="codicon codicon-extensions" style={{ marginRight: "6px" }}></span>
{t("mcp:marketplace")}
</Button>
</StandardTooltip>
*/}
<Button
variant="secondary"
style={{ width: "100%" }}
onClick={() => {
// TODO: Implement this
}}>
<span className="codicon codicon-add" style={{ marginRight: "6px" }}></span>
{t("mcp:addUrlBasedMcp")}
</Button>
</div>
{/* kilocode_change start */}
<div className="mt-5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
Loading