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
8 changes: 8 additions & 0 deletions src/api/providers/kilocode-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,11 @@ export const KILO_CODE_MODELS: Record<string, KiloCodeModel> = {
},
},
}

/**
* Checks whether a given model ID is present in the static KiloCode model list.
* Used to detect stale model selections after extension updates remove models.
*/
export function isValidKilocodeModel(modelId: string): boolean {
return modelId in KILO_CODE_MODELS
}
8 changes: 8 additions & 0 deletions src/api/providers/kilocode-openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ export class KilocodeOpenrouterHandler extends OpenRouterHandler {

override getModel() {
let id = this.options.kilocodeModel ?? this.defaultModel

// Safety net: if the selected model is not in the fetched model list,
// fall back to the default. This handles stale config values that were
// not yet caught by ClineProvider's validation during initialization.
if (id && !this.models[id]) {
id = this.defaultModel
}

let info = this.models[id] ?? openRouterDefaultModelInfo

// If a specific provider is requested, use the endpoint for that provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default {
maxItems: 4,
},
},
required: ["question", "follow_up"],
required: ["question"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/browser_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default {
description: "Text to type when performing the type action",
},
},
required: ["action", "url", "coordinate", "size", "text"],
required: ["action"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/codebase_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
description: "Optional subdirectory (relative to the workspace) to limit the search scope",
},
},
required: ["query", "path"],
required: ["query"],
additionalProperties: false,
},
},
Expand Down
7 changes: 6 additions & 1 deletion src/core/prompts/tools/native-tools/execute_command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ export default {
type: ["string", "null"],
description: "Optional working directory for the command, relative or absolute",
},
message: {
type: "string",
description:
"A clear, concise one-line description of what the command does, shown to the user for approval (e.g. 'Install project dependencies with npm')",
},
},
required: ["command", "cwd"],
required: ["command"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/generate_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default {
"Optional path (relative to the workspace) to an existing image to edit; supports PNG, JPG, JPEG, GIF, and WEBP",
},
},
required: ["prompt", "path", "image"],
required: ["prompt", "path"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/list_files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
description: "Set true to list contents recursively; omit or false to show only the top level",
},
},
required: ["path", "recursive"],
required: ["path"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/new_task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default {
"Optional initial todo list written as a markdown checklist; required when the workspace mandates todos",
},
},
required: ["mode", "message", "todos"],
required: ["mode", "message"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/run_slash_command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
description: "Optional additional context or arguments for the command",
},
},
required: ["command", "args"],
required: ["command"],
additionalProperties: false,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/prompts/tools/native-tools/switch_mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
description: "Optional explanation for why the mode switch is needed",
},
},
required: ["mode_slug", "reason"],
required: ["mode_slug"],
additionalProperties: false,
},
},
Expand Down
3 changes: 2 additions & 1 deletion src/core/tools/codebaseSearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export async function codebaseSearchTool(

// --- Parameter Extraction and Validation ---
let query: string | undefined = block.params.query
let directoryPrefix: string | undefined = block.params.path
const rawPath: string | undefined = block.params.path
let directoryPrefix: string | undefined = rawPath && rawPath !== "null" ? rawPath : undefined

query = removeClosingTag("query", query)

Expand Down
9 changes: 7 additions & 2 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export async function executeCommandTool(
removeClosingTag: RemoveClosingTag,
) {
let command: string | undefined = block.params.command
const customCwd: string | undefined = block.params.cwd
const rawCwd: string | undefined = block.params.cwd
const customCwd: string | undefined = rawCwd && rawCwd !== "null" ? rawCwd : undefined
const rawMessage: unknown = block.params.message
const customMessage: string | undefined =
typeof rawMessage === "string" && rawMessage !== "null" ? rawMessage : undefined

try {
if (block.partial) {
Expand All @@ -54,7 +58,8 @@ export async function executeCommandTool(
task.consecutiveMistakeCount = 0

command = unescapeHtmlEntities(command) // Unescape HTML entities.
const didApprove = await askApproval("command", command)
const askText = customMessage ? `MESSAGE:${customMessage}\n---\n${command}` : command
const didApprove = await askApproval("command", askText)

if (!didApprove) {
return
Expand Down
17 changes: 17 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { OpenRouterHandler } from "../../api/providers"
import { stringifyError } from "../../shared/kilocode/errorUtils"
import isWsl from "is-wsl"
import { getKilocodeDefaultModel } from "../../api/providers/kilocode/getKilocodeDefaultModel"
import { isValidKilocodeModel } from "../../api/providers/kilocode-models"
import { getKiloCodeWrapperProperties } from "../../core/kilocode/wrapper"
import { getKiloUrlFromToken } from "@roo-code/types" // kilocode_change
import { getKilocodeConfig, getWorkspaceProjectId, KilocodeConfig } from "../../utils/kilo-config-file" // kilocode_change
Expand Down Expand Up @@ -2290,6 +2291,22 @@ ${prompt}
}
}

// Validate global kilocodeModel against available models.
// If the stored model ID was removed from the extension's model list
// (e.g. after an update), force-reset to the default model.
if (apiConfiguration?.apiProvider === "kilocode" && apiConfiguration?.kilocodeModel) {
if (!isValidKilocodeModel(apiConfiguration.kilocodeModel)) {
const staleModel = apiConfiguration.kilocodeModel
const defaultModel = await getKilocodeDefaultModel()
const updatedConfig = { ...apiConfiguration, kilocodeModel: defaultModel }
await this.contextProxy.setProviderSettings(updatedConfig)
mergedApiConfiguration = { ...mergedApiConfiguration, kilocodeModel: defaultModel }
this.log(
`[ModelValidation] Reset stale kilocodeModel "${staleModel}" to default "${defaultModel}" — model no longer available`,
)
}
}

// forked_change start wrapper information
const kiloCodeWrapperProperties = getKiloCodeWrapperProperties()
const taskHistory = this.getTaskHistory()
Expand Down
68 changes: 68 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,74 @@ describe("ClineProvider", () => {
})
})

describe("model validation on stale kilocodeModel", () => {
let logSpy: any

beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
logSpy = vi.spyOn(provider, "log").mockImplementation(() => {})
})

afterEach(() => {
logSpy.mockRestore()
})

test("resets stale kilocodeModel to default when model not in KILO_CODE_MODELS", async () => {
const { isValidKilocodeModel } = await import("../../../api/providers/kilocode-models")

const setSettingsSpy = vi.spyOn(provider.contextProxy, "setProviderSettings").mockResolvedValue()
const getSettingsSpy = vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({
apiProvider: "kilocode",
kilocodeModel: "non-existent-model-12345",
kilocodeToken: "test-token",
} as any)

expect(isValidKilocodeModel("non-existent-model-12345")).toBe(false)

const state = await provider.getState()

expect(state.apiConfiguration?.kilocodeModel).not.toBe("non-existent-model-12345")
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("[ModelValidation] Reset stale kilocodeModel"))
expect(setSettingsSpy).toHaveBeenCalled()
getSettingsSpy.mockRestore()
setSettingsSpy.mockRestore()
})

test("preserves valid kilocodeModel when it exists in KILO_CODE_MODELS", async () => {
const { isValidKilocodeModel, KILO_CODE_MODELS } = await import("../../../api/providers/kilocode-models")

const validModelId = Object.keys(KILO_CODE_MODELS)[0]
expect(isValidKilocodeModel(validModelId)).toBe(true)

const getSettingsSpy = vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({
apiProvider: "kilocode",
kilocodeModel: validModelId,
kilocodeToken: "test-token",
} as any)

const state = await provider.getState()

expect(state.apiConfiguration?.kilocodeModel).toBe(validModelId)
expect(logSpy).not.toHaveBeenCalledWith(
expect.stringContaining("[ModelValidation] Reset stale kilocodeModel"),
)
getSettingsSpy.mockRestore()
})

test("skips validation when apiProvider is not kilocode", async () => {
const getSettingsSpy = vi.spyOn(provider.contextProxy, "getProviderSettings").mockReturnValue({
apiProvider: "openrouter",
openRouterModelId: "some-openrouter-model",
} as any)

const state = await provider.getState()

expect(state.apiConfiguration?.apiProvider).toBe("openrouter")
expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining("[ModelValidation]"))
getSettingsSpy.mockRestore()
})
})

describe("updateCustomMode", () => {
test("updates both file and state when updating custom mode", async () => {
await provider.resolveWebviewView(mockWebviewView)
Expand Down
8 changes: 8 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ClineProvider } from "./core/webview/ClineProvider"
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import { PlanEditorProvider } from "./integrations/editor/PlanEditorProvider"
import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { captureShellEnvironment, setShellLogger } from "./integrations/terminal/ShellEnvironment"
import { McpServerManager } from "./services/mcp/McpServerManager"
import { CodeIndexManager } from "./services/code-index/manager"
import { registerCommitMessageProvider } from "./services/commit-message"
Expand Down Expand Up @@ -155,6 +156,13 @@ export async function activate(context: vscode.ExtensionContext) {
// Initialize terminal shell execution handlers.
TerminalRegistry.initialize()

// Eagerly capture the user's login shell environment so that CLI tools
// installed via Homebrew, nvm, cargo, etc. are on PATH even when VS Code
// was launched from macOS Dock/Finder (which inherits launchd's restricted
// PATH instead of the user's shell rc files).
setShellLogger((msg: string) => outputChannel.appendLine(msg))
captureShellEnvironment()

// Get default commands from configuration.
const defaultCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []

Expand Down
16 changes: 14 additions & 2 deletions src/integrations/terminal/ExecaTerminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import process from "process"

import type { RooTerminal } from "./types"
import { BaseTerminalProcess } from "./BaseTerminalProcess"
import { getShellEnvironment, getCapturedShell } from "./ShellEnvironment"

export class ExecaTerminalProcess extends BaseTerminalProcess {
private terminalRef: WeakRef<RooTerminal>
Expand Down Expand Up @@ -38,13 +39,24 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
try {
this.isHot = true

// Use the user's real login shell (bash/zsh) instead of the
// default /bin/sh so that shell syntax matches user expectations.
// On Windows, process.env already carries the full system PATH;
// using shell:true (cmd.exe) is the safest default there.
const shellPath = process.platform === "win32" ? true : getCapturedShell()

// Use the captured login-shell environment so that CLI tools
// installed via Homebrew, nvm, cargo, etc. are on PATH even when
// VS Code was launched from the macOS Dock/Finder.
const shellEnv = getShellEnvironment()

this.subprocess = execa({
shell: true,
shell: shellPath,
cwd: this.terminal.getCurrentWorkingDirectory(),
all: true,
stdin: "ignore", // kilocode_change: ignore stdin to prevent blocking
env: {
...process.env,
...shellEnv,
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
Expand Down
Loading
Loading