Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [v5.7.3] - 2026-04-02

### Added

- OAuth authentication support for MCP servers
- LSP tool for code definition navigation
- file_write tool implementation

### Changed

- Modernized settings panel UI with new card-based design
- Reduced VSCodeButton size by 5% across all variants for better visual consistency

### Fixed

- Allow changing 3p models for existing tasks and fixed model display names

---

## [v5.7.2] - 2026-03-29

### Added
Expand Down
26 changes: 26 additions & 0 deletions cli/src/constants/providers/__tests__/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@
describe("prettyModelName", () => {
it("should remove common prefixes", () => {
expect(prettyModelName("anthropic.claude-3-opus")).toBe("Claude 3 Opus")
expect(prettyModelName("deepseek-ai/DeepSeek-R1")).toBe("DeepSeek R1")

Check failure on line 601 in cli/src/constants/providers/__tests__/models.test.ts

View workflow job for this annotation

GitHub Actions / test-cli

src/constants/providers/__tests__/models.test.ts > Static Provider Models > prettyModelName > should remove common prefixes

AssertionError: expected 'Deepseek-ai / DeepSeek R1' to be 'DeepSeek R1' // Object.is equality Expected: "DeepSeek R1" Received: "Deepseek-ai / DeepSeek R1" ❯ src/constants/providers/__tests__/models.test.ts:601:55
})

it("should convert dashes to spaces", () => {
Expand All @@ -608,6 +608,32 @@
it("should capitalize words", () => {
expect(prettyModelName("claude-sonnet")).toBe("Claude Sonnet")
})

it("should handle three-part paths with @cf prefix", () => {
// @cf/moonshotai/kimi-k2.5 -> Kimi K2.5 (Moonshotai)
expect(prettyModelName("@cf/moonshotai/kimi-k2.5")).toBe("Kimi K2.5 (Moonshotai)")
expect(prettyModelName("@cf/openai/gpt-4")).toBe("Gpt 4 (Openai)")
})

it("should handle three-part paths with provider prefix", () => {
// matterai3p:@cf/moonshotai/kimi-k2.5 -> Kimi K2.5 (Moonshotai)
expect(prettyModelName("matterai3p:@cf/moonshotai/kimi-k2.5")).toBe("Kimi K2.5 (Moonshotai)")
expect(prettyModelName("ollama:llama3.2:latest")).toBe("Llama3.2 (Latest)")
})

it("should handle two-part paths", () => {
expect(prettyModelName("openai/gpt-4")).toBe("Openai / Gpt 4")
expect(prettyModelName("deepseek-ai/DeepSeek-R1")).toBe("DeepSeek R1")

Check failure on line 626 in cli/src/constants/providers/__tests__/models.test.ts

View workflow job for this annotation

GitHub Actions / test-cli

src/constants/providers/__tests__/models.test.ts > Static Provider Models > prettyModelName > should handle two-part paths

AssertionError: expected 'Deepseek-ai / DeepSeek R1' to be 'DeepSeek R1' // Object.is equality Expected: "DeepSeek R1" Received: "Deepseek-ai / DeepSeek R1" ❯ src/constants/providers/__tests__/models.test.ts:626:55
})

it("should handle model IDs with colons (tags)", () => {
expect(prettyModelName("llama3.2:latest")).toBe("Llama3.2 (Latest)")
expect(prettyModelName("ollama:llama3.2:latest")).toBe("Llama3.2 (Latest)")
})

it("should handle empty or null input", () => {
expect(prettyModelName("")).toBe("")
})
})

describe("Model Properties Validation", () => {
Expand Down
60 changes: 57 additions & 3 deletions cli/src/constants/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,63 @@ export function fuzzyFilterModels(models: ModelRecord, filter: string): string[]

/**
* Get a pretty name for a model
* Handles various model ID formats:
* - Simple names: "gpt-4" -> "Gpt 4"
* - Two-part paths: "openai/gpt-4" -> "Openai / Gpt 4"
* - Three-part paths: "@cf/moonshotai/kimi-k2.5" -> "Kimi K2.5 (Moonshotai)"
* - With tags: "llama3.2:latest" -> "Llama3.2 (Latest)"
*/
export function prettyModelName(modelId: string): string {
// Remove common prefixes
let name = modelId
if (!modelId) {
return ""
}

// Remove provider prefix if present (e.g., "matterai3p:", "ollama:", "opencode:", "fireworks:")
const withoutProviderPrefix = modelId.replace(/^(matterai3p|ollama|opencode|fireworks):/, "")

const [mainId, tag] = withoutProviderPrefix.split(":")

// Handle paths with "/" separator
if (mainId?.includes("/")) {
const segments = mainId.split("/").filter(Boolean)

// Handle three-part paths like "@cf/moonshotai/kimi-k2.5"
// Pattern: [prefix]/[vendor]/[model] or [vendor]/[model]
if (segments.length >= 3) {
// Take the last segment as model name
const modelName = segments[segments.length - 1]!
// Take the second-to-last as vendor (skip prefixes like "@cf")
const vendor = segments[segments.length - 2]!

// Format model name: replace hyphens/underscores with spaces, title case
const formattedModelName = modelName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())

// Format vendor: title case
const formattedVendor = vendor.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())

const formattedTag = tag ? ` (${tag.charAt(0).toUpperCase() + tag.slice(1)})` : ""
return `${formattedModelName} (${formattedVendor})${formattedTag}`
}

// Handle two-part paths like "openai/gpt-4"
if (segments.length === 2) {
const projectName = segments[0]!
const modelName = segments[1]!

const formattedProject = projectName.charAt(0).toUpperCase() + projectName.slice(1)
const formattedName = modelName
.split("-")
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")

const formattedTag = tag ? ` (${tag.charAt(0).toUpperCase() + tag.slice(1)})` : ""
return `${formattedProject} / ${formattedName}${formattedTag}`
}
}

// Remove common prefixes for simple names
let name = (mainId || "")
.replace(/^anthropic\./, "")
.replace(/^accounts\/fireworks\/models\//, "")
.replace(/^deepseek-ai\//, "")
Expand All @@ -578,5 +631,6 @@ export function prettyModelName(modelId: string): string {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")

return name
const formattedTag = tag ? ` (${tag.charAt(0).toUpperCase() + tag.slice(1)})` : ""
return name + formattedTag
}
3 changes: 3 additions & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ export const toolNames = [
"write_to_file",
"apply_diff",
"file_edit",
"file_write",
"insert_content",
"search_and_replace",
"search_files",
"list_files",
"list_code_definition_names",
"lsp",
"browser_action",
"use_mcp_tool",
"access_mcp_resource",
"mcp_authenticate",
"ask_followup_question",
"attempt_completion",
"switch_mode",
Expand Down
46 changes: 46 additions & 0 deletions src/activate/handleUri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as vscode from "vscode"
import { CloudService } from "@roo-code/cloud"

import { ClineProvider } from "../core/webview/ClineProvider"
import { McpOAuthCallbackManager } from "../services/mcp/oauth-callback"
import { McpOAuthProvider } from "../services/mcp/oauth-provider"

export const handleUri = async (uri: vscode.Uri) => {
const path = uri.path
Expand Down Expand Up @@ -66,6 +68,50 @@ export const handleUri = async (uri: vscode.Uri) => {
)
break
}
case "/mcp/oauth/callback": {
// Handle MCP OAuth callback
const code = query.get("code")
const state = query.get("state")
const serverName = query.get("server_name")

if (code && state) {
try {
// Get the pending auth data from callback manager
const callbackManager = McpOAuthCallbackManager.getInstance()
const authData = callbackManager.getPendingAuth(state)

if (authData) {
// Create OAuth provider to save tokens
const context = visibleProvider.context
const oauthProvider = new McpOAuthProvider(context)

// Complete the OAuth flow and save tokens
const tokens = await oauthProvider.completeOAuthFlow(authData.serverName, code, state)

// Clean up pending auth data
callbackManager.clearPendingAuth(state)

// Get the MCP hub and reconnect the server
const mcpHub = visibleProvider.getMcpHub()
if (mcpHub) {
// Reconnect the server with the new tokens
await mcpHub.reconnectServer(authData.serverName)
}

vscode.window.showInformationMessage(
`Successfully authenticated with ${authData.serverName}. The server is now reconnecting...`,
)
} else {
// Fall back to the callback manager's handleCallback
await callbackManager.handleCallback(code, state, serverName || undefined)
}
} catch (error) {
console.error("[MCP OAuth] Failed to complete OAuth flow:", error)
vscode.window.showErrorMessage(`Failed to complete authentication: ${(error as Error).message}`)
}
}
break
}
default:
break
}
Expand Down
19 changes: 14 additions & 5 deletions src/api/providers/kilocode/nativeToolCallHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ import type { ApiStreamNativeToolCallsChunk } from "../../transform/kilocode/api
export function addNativeToolCallsToParams<T extends OpenAI.Chat.ChatCompletionCreateParams>(
params: T,
_options: ProviderSettings,
_metadata?: ApiHandlerCreateMessageMetadata,
metadata?: ApiHandlerCreateMessageMetadata,
): T {
// When toolStyle is "json", always add all native tools

// Use allowedTools if provided, otherwise use all native tools
const tools = nativeTools
// When toolStyle is "json", add tool definitions to the API request.
// Use allowedTools from metadata if provided (includes mode-filtered native tools + MCP tools),
// otherwise fall back to the default set of all native tools.
const tools = metadata?.allowedTools && metadata.allowedTools.length > 0 ? metadata.allowedTools : nativeTools
if (tools && tools.length > 0) {
params.tools = tools
//optimally we'd have tool_choice as 'required', but many providers, especially
Expand Down Expand Up @@ -120,6 +120,15 @@ export function* processNativeToolCallsFromDelta(
// Map to the ApiStreamNativeToolCallsChunk format
const validToolCalls = delta.tool_calls
.filter((tc) => tc.function) // Keep any delta with function data
.filter((tc) => {
// Skip tool calls with null/empty names when id is also null
// These are placeholder entries in the delta stream
const hasValidId = tc.id !== null && tc.id !== undefined
const hasValidName =
tc.function!.name !== null && tc.function!.name !== undefined && tc.function!.name !== ""
// Keep if we have a valid id OR a valid name (one will be present in valid calls)
return hasValidId || hasValidName
})
.map((tc) => ({
index: tc.index, // Use index to track across deltas
id: tc.id, // Only present in first delta
Expand Down
86 changes: 69 additions & 17 deletions src/core/assistant-message/AssistantMessageParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { AssistantMessageContent } from "./parseAssistantMessage"
import { NativeToolCall, parseDoubleEncodedParams } from "./kilocode/native-tool-call"
import Anthropic from "@anthropic-ai/sdk" // kilocode_change

/**
* Callback function type to check if a tool name is a valid MCP tool.
* Returns the server name if it's an MCP tool, or undefined if not.
*/
export type McpToolChecker = (toolName: string) => { isMcpTool: boolean; serverName?: string } | undefined

/**
* Parser for assistant messages. Maintains state between chunks
* to avoid reprocessing the entire message on each update.
Expand All @@ -25,14 +31,17 @@ export class AssistantMessageParser {
private processedNativeToolCallIds: Set<string> = new Set()
// Map index to id for tracking across streaming deltas
private nativeToolCallIndexToId: Map<number, string> = new Map()
// Callback to check if a tool name is an MCP tool
private mcpToolChecker: McpToolChecker | undefined
// forked_change end

private accumulator = ""

/**
* Initialize a new AssistantMessageParser instance.
*/
constructor() {
constructor(mcpToolChecker?: McpToolChecker) {
this.mcpToolChecker = mcpToolChecker
this.reset()
}

Expand Down Expand Up @@ -119,8 +128,12 @@ export class AssistantMessageParser {
if (toolCall.function?.name) {
const toolName = toolCall.function.name

// Validate that this is a recognized tool name
if (!toolNames.includes(toolName as ToolName)) {
// Validate that this is a recognized tool name (native or MCP)
const isNativeTool = toolNames.includes(toolName as ToolName)
const mcpCheck = this.mcpToolChecker?.(toolName)
const isMcpTool = mcpCheck?.isMcpTool ?? false

if (!isNativeTool && !isMcpTool) {
console.warn("[AssistantMessageParser] Unknown tool name in native call:", toolName)
continue
}
Expand All @@ -133,6 +146,9 @@ export class AssistantMessageParser {
name: toolCall.function.name,
arguments: toolCall.function.arguments || "",
},
// forked_change: Track if this is an MCP tool and which server
isMcpTool: isMcpTool,
mcpServerName: mcpCheck?.serverName,
}
this.nativeToolCallsAccumulator.set(toolCallId, accumulatedCall)
} else {
Expand Down Expand Up @@ -189,13 +205,32 @@ export class AssistantMessageParser {
this.currentTextContent = undefined
}

// Create a ToolUse block from the native tool call
const toolUse: ToolUse = {
type: "tool_use",
name: toolName as ToolName,
params: parsedArgs,
partial: false, // Now complete after accumulation
toolUseId: accumulatedCall.id,
// forked_change: Handle MCP tools by converting to use_mcp_tool
let toolUse: ToolUse
if (accumulatedCall.isMcpTool && accumulatedCall.mcpServerName) {
// Convert MCP tool call to use_mcp_tool format
console.log("[MCP Debug] Converting native MCP tool call:", toolName, "args:", parsedArgs)
toolUse = {
type: "tool_use",
name: "use_mcp_tool" as ToolName,
params: {
server_name: accumulatedCall.mcpServerName,
tool_name: toolName,
arguments: JSON.stringify(parsedArgs),
},
partial: false,
toolUseId: accumulatedCall.id,
}
console.log("[MCP Debug] Converted to use_mcp_tool:", toolUse.params)
} else {
// Create a ToolUse block from the native tool call
toolUse = {
type: "tool_use",
name: toolName as ToolName,
params: parsedArgs,
partial: false, // Now complete after accumulation
toolUseId: accumulatedCall.id,
}
}

// Add the tool use to content blocks
Expand Down Expand Up @@ -459,13 +494,30 @@ export class AssistantMessageParser {
}

const toolName = accumulatedCall.function!.name
// Create a ToolUse block from the native tool call
const toolUse: ToolUse = {
type: "tool_use",
name: toolName as ToolName,
params: parsedArgs,
partial: false,
toolUseId: accumulatedCall.id,
// forked_change: Handle MCP tools by converting to use_mcp_tool
let toolUse: ToolUse
if (accumulatedCall.isMcpTool && accumulatedCall.mcpServerName) {
// Convert MCP tool call to use_mcp_tool format
toolUse = {
type: "tool_use",
name: "use_mcp_tool" as ToolName,
params: {
server_name: accumulatedCall.mcpServerName,
tool_name: toolName,
arguments: JSON.stringify(parsedArgs),
},
partial: false,
toolUseId: accumulatedCall.id,
}
} else {
// Create a ToolUse block from the native tool call
toolUse = {
type: "tool_use",
name: toolName as ToolName,
params: parsedArgs,
partial: false,
toolUseId: accumulatedCall.id,
}
}

// Add the tool use to content blocks
Expand Down
3 changes: 3 additions & 0 deletions src/core/assistant-message/kilocode/native-tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export interface NativeToolCall {
name: string
arguments: string // JSON string (may be partial during streaming)
}
// forked_change: Track if this is an MCP tool and which server
isMcpTool?: boolean
mcpServerName?: string
}

/**
Expand Down
Loading
Loading