diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs index 5c585923e45..ad37f5b319b 100644 --- a/cli/eslint.config.mjs +++ b/cli/eslint.config.mjs @@ -1,13 +1,34 @@ -import { config } from "@roo-code/config-eslint/base" +import js from "@eslint/js" +import eslintConfigPrettier from "eslint-config-prettier" +import turboPlugin from "eslint-plugin-turbo" +import tseslint from "typescript-eslint" export default [ - ...config, + js.configs.recommended, + eslintConfigPrettier, + ...tseslint.configs.recommended, + { + plugins: { + turbo: turboPlugin, + }, + rules: { + "turbo/no-undeclared-env-vars": "off", + }, + }, { rules: { - // "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "error", }, }, { - ignores: ["dist/*"], + ignores: ["dist/*", "scripts/*"], }, ] diff --git a/cli/package.json b/cli/package.json index 42e9eb12365..72dc5e7845c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -148,6 +148,7 @@ } }, "devDependencies": { + "@eslint/js": "^9.22.0", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", "@types/fs-extra": "^11.0.4", @@ -157,12 +158,15 @@ "@types/semver": "^7.5.8", "cpy-cli": "^5.0.0", "del-cli": "^5.1.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-turbo": "^2.4.4", "ink-testing-library": "^4.0.0", "mkdirp": "^3.0.1", "prettier": "^3.4.2", "rimraf": "^6.1.0", "tsx": "^4.19.3", "typescript": "^5.4.5", + "typescript-eslint": "^8.26.0", "vitest": "^3.2.3" }, "engines": { diff --git a/cli/src/__tests__/config-command.test.ts b/cli/src/__tests__/config-command.test.ts index 27226895d38..3acfdded08a 100644 --- a/cli/src/__tests__/config-command.test.ts +++ b/cli/src/__tests__/config-command.test.ts @@ -11,7 +11,7 @@ vi.mock("fs/promises", async () => { const actual = await vi.importActual("fs/promises") return { ...actual, - readFile: vi.fn(async (filePath: any, encoding?: any) => { + readFile: vi.fn(async (filePath: string | Buffer | URL, encoding?: BufferEncoding | null) => { // If reading schema.json, return a minimal valid schema if (typeof filePath === "string" && filePath.includes("schema.json")) { return JSON.stringify({ @@ -21,7 +21,7 @@ vi.mock("fs/promises", async () => { }) } // Otherwise use the actual implementation - return actual.readFile(filePath, encoding) + return actual.readFile(filePath, encoding as BufferEncoding) }), } }) diff --git a/cli/src/__tests__/config-default-creation.test.ts b/cli/src/__tests__/config-default-creation.test.ts index 9a758b71f2d..17b14daad6a 100644 --- a/cli/src/__tests__/config-default-creation.test.ts +++ b/cli/src/__tests__/config-default-creation.test.ts @@ -20,7 +20,7 @@ vi.mock("fs/promises", async () => { const actual = await vi.importActual("fs/promises") return { ...actual, - readFile: vi.fn(async (filePath: any, encoding?: any) => { + readFile: vi.fn(async (filePath: string | Buffer | URL, encoding?: BufferEncoding | null) => { // If reading schema.json, return a minimal valid schema if (typeof filePath === "string" && filePath.includes("schema.json")) { return JSON.stringify({ @@ -30,7 +30,7 @@ vi.mock("fs/promises", async () => { }) } // Otherwise use the actual implementation - return actual.readFile(filePath, encoding) + return actual.readFile(filePath, encoding as BufferEncoding) }), } }) diff --git a/cli/src/__tests__/history.test.ts b/cli/src/__tests__/history.test.ts index 570eafcf64a..b4b1a4685e2 100644 --- a/cli/src/__tests__/history.test.ts +++ b/cli/src/__tests__/history.test.ts @@ -2,7 +2,7 @@ * Tests for history persistence and navigation */ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import { describe, it, expect, beforeEach, afterEach } from "vitest" import * as fs from "fs/promises" import * as path from "path" import * as os from "os" @@ -34,7 +34,7 @@ describe("History Persistence", () => { // Clean up test directory try { await fs.rm(testDir, { recursive: true, force: true }) - } catch (error) { + } catch (_error) { // Ignore cleanup errors } diff --git a/cli/src/commands/__tests__/clear.test.ts b/cli/src/commands/__tests__/clear.test.ts index 1d4ff565310..f341496be03 100644 --- a/cli/src/commands/__tests__/clear.test.ts +++ b/cli/src/commands/__tests__/clear.test.ts @@ -2,52 +2,18 @@ * Tests for the /clear command */ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import { describe, it, expect, beforeEach, vi } from "vitest" import { clearCommand } from "../clear.js" import type { CommandContext } from "../core/types.js" +import { createMockContext } from "./helpers/mockContext.js" describe("clearCommand", () => { let mockContext: CommandContext beforeEach(() => { - mockContext = { + mockContext = createMockContext({ input: "/clear", - args: [], - options: {}, - sendMessage: vi.fn().mockResolvedValue(undefined), - addMessage: vi.fn(), - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - setMessageCutoffTimestamp: vi.fn(), - clearTask: vi.fn().mockResolvedValue(undefined), - setMode: vi.fn(), - exit: vi.fn(), - routerModels: null, - currentProvider: null, - kilocodeDefaultModel: "", - updateProviderModel: vi.fn().mockResolvedValue(undefined), - refreshRouterModels: vi.fn().mockResolvedValue(undefined), - updateProvider: vi.fn().mockResolvedValue(undefined), - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - refreshTerminal: vi.fn().mockResolvedValue(undefined), - taskHistoryData: null, - taskHistoryFilters: { - workspace: "current", - sort: "newest", - favoritesOnly: false, - }, - taskHistoryLoading: false, - taskHistoryError: null, - fetchTaskHistory: vi.fn().mockResolvedValue(undefined), - updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), - changeTaskHistoryPage: vi.fn().mockResolvedValue(null), - nextTaskHistoryPage: vi.fn().mockResolvedValue(null), - previousTaskHistoryPage: vi.fn().mockResolvedValue(null), - sendWebviewMessage: vi.fn().mockResolvedValue(undefined), - } + }) }) describe("command metadata", () => { @@ -87,7 +53,7 @@ describe("clearCommand", () => { const afterTime = Date.now() expect(mockContext.setMessageCutoffTimestamp).toHaveBeenCalledTimes(1) - const timestamp = (mockContext.setMessageCutoffTimestamp as any).mock.calls[0][0] + const timestamp = (mockContext.setMessageCutoffTimestamp as ReturnType).mock.calls[0][0] expect(timestamp).toBeGreaterThanOrEqual(beforeTime) expect(timestamp).toBeLessThanOrEqual(afterTime) }) diff --git a/cli/src/commands/__tests__/config.test.ts b/cli/src/commands/__tests__/config.test.ts index 299f1d1236c..507b27ffb91 100644 --- a/cli/src/commands/__tests__/config.test.ts +++ b/cli/src/commands/__tests__/config.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { configCommand } from "../config.js" import type { CommandContext } from "../core/types.js" import openConfigFile from "../../config/openConfig.js" +import { createMockContext } from "./helpers/mockContext.js" // Mock the openConfigFile function vi.mock("../../config/openConfig.js", () => ({ @@ -16,28 +17,10 @@ describe("configCommand", () => { vi.clearAllMocks() addMessageSpy = vi.fn() - mockContext = { + mockContext = createMockContext({ input: "/config", - args: [], - options: {}, - sendMessage: vi.fn(), addMessage: addMessageSpy, - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - clearTask: vi.fn(), - setMode: vi.fn(), - exit: vi.fn(), - routerModels: null, - currentProvider: null, - kilocodeDefaultModel: "", - updateProviderModel: vi.fn(), - refreshRouterModels: vi.fn(), - updateProvider: vi.fn(), - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - } + }) }) it("should have correct metadata", () => { diff --git a/cli/src/commands/__tests__/helpers/mockContext.ts b/cli/src/commands/__tests__/helpers/mockContext.ts new file mode 100644 index 00000000000..87fde4ad06a --- /dev/null +++ b/cli/src/commands/__tests__/helpers/mockContext.ts @@ -0,0 +1,78 @@ +/** + * Test helper for creating mock CommandContext + * + * This utility provides a reusable mock context for command tests, + * eliminating the need to duplicate the same mock setup across multiple test files. + * + * @example + * ```typescript + * import { createMockContext } from "./helpers/mockContext.js" + * + * const mockContext = createMockContext({ + * input: "/clear", + * addMessage: vi.fn(), + * }) + * ``` + */ + +import { vi } from "vitest" +import type { CommandContext } from "../../core/types.js" +import type { CLIConfig } from "../../../config/types.js" + +/** + * Creates a mock CommandContext with all required properties. + * All functions are mocked with vi.fn() and return appropriate default values. + * + * @param overrides - Partial context to override default values + * @returns A complete mock CommandContext with all required properties + */ +export function createMockContext(overrides: Partial = {}): CommandContext { + const defaultContext: CommandContext = { + input: "", + args: [], + options: {}, + config: {} as CLIConfig, + sendMessage: vi.fn().mockResolvedValue(undefined), + addMessage: vi.fn(), + clearMessages: vi.fn(), + replaceMessages: vi.fn(), + setMessageCutoffTimestamp: vi.fn(), + clearTask: vi.fn().mockResolvedValue(undefined), + setMode: vi.fn(), + setTheme: vi.fn().mockResolvedValue(undefined), + exit: vi.fn(), + setCommittingParallelMode: vi.fn(), + isParallelMode: false, + routerModels: null, + currentProvider: null, + kilocodeDefaultModel: "", + updateProviderModel: vi.fn().mockResolvedValue(undefined), + refreshRouterModels: vi.fn().mockResolvedValue(undefined), + updateProvider: vi.fn().mockResolvedValue(undefined), + profileData: null, + balanceData: null, + profileLoading: false, + balanceLoading: false, + refreshTerminal: vi.fn().mockResolvedValue(undefined), + taskHistoryData: null, + taskHistoryFilters: { + workspace: "current", + sort: "newest", + favoritesOnly: false, + }, + taskHistoryLoading: false, + taskHistoryError: null, + fetchTaskHistory: vi.fn().mockResolvedValue(undefined), + updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), + changeTaskHistoryPage: vi.fn().mockResolvedValue(null), + nextTaskHistoryPage: vi.fn().mockResolvedValue(null), + previousTaskHistoryPage: vi.fn().mockResolvedValue(null), + sendWebviewMessage: vi.fn().mockResolvedValue(undefined), + chatMessages: [], + } + + return { + ...defaultContext, + ...overrides, + } +} diff --git a/cli/src/commands/__tests__/model.autocomplete.test.ts b/cli/src/commands/__tests__/model.autocomplete.test.ts index a286c815018..dc7f135e256 100644 --- a/cli/src/commands/__tests__/model.autocomplete.test.ts +++ b/cli/src/commands/__tests__/model.autocomplete.test.ts @@ -6,9 +6,10 @@ import { describe, it, expect, beforeEach } from "vitest" import { getArgumentSuggestions } from "../../services/autocomplete.js" import type { RouterModels } from "../../types/messages.js" import type { ProviderConfig } from "../../config/types.js" +import type { ArgumentProviderContext } from "../core/types.js" describe("Model Command Autocomplete", () => { - let mockCommandContext: any + let mockCommandContext: Partial beforeEach(() => { // Mock command context with router models @@ -71,7 +72,10 @@ describe("Model Command Autocomplete", () => { // multi-argument commands. The fix works in the real application where // autocomplete is triggered through the UI differently. const input = "/model select gpt" - const suggestions = await getArgumentSuggestions(input, mockCommandContext) + const suggestions = await getArgumentSuggestions( + input, + mockCommandContext as ArgumentProviderContext["commandContext"], + ) expect(suggestions).toBeDefined() expect(suggestions.length).toBeGreaterThan(0) @@ -86,7 +90,10 @@ describe("Model Command Autocomplete", () => { // multi-argument commands. The fix works in the real application where // autocomplete is triggered through the UI differently. const input = "/model select " - const suggestions = await getArgumentSuggestions(input, mockCommandContext) + const suggestions = await getArgumentSuggestions( + input, + mockCommandContext as ArgumentProviderContext["commandContext"], + ) expect(suggestions).toBeDefined() expect(suggestions.length).toBe(3) // All 3 mock models @@ -97,7 +104,10 @@ describe("Model Command Autocomplete", () => { // multi-argument commands. The fix works in the real application where // autocomplete is triggered through the UI differently. const input = "/model select claude" - const suggestions = await getArgumentSuggestions(input, mockCommandContext) + const suggestions = await getArgumentSuggestions( + input, + mockCommandContext as ArgumentProviderContext["commandContext"], + ) expect(suggestions).toBeDefined() expect(suggestions.length).toBeGreaterThan(0) @@ -121,7 +131,10 @@ describe("Model Command Autocomplete", () => { ...mockCommandContext, currentProvider: null, } - const suggestions = await getArgumentSuggestions(input, contextWithoutProvider) + const suggestions = await getArgumentSuggestions( + input, + contextWithoutProvider as ArgumentProviderContext["commandContext"], + ) expect(suggestions).toBeDefined() expect(suggestions.length).toBe(0) @@ -133,7 +146,10 @@ describe("Model Command Autocomplete", () => { ...mockCommandContext, routerModels: null, } - const suggestions = await getArgumentSuggestions(input, contextWithoutModels) + const suggestions = await getArgumentSuggestions( + input, + contextWithoutModels as ArgumentProviderContext["commandContext"], + ) expect(suggestions).toBeDefined() expect(suggestions.length).toBe(0) diff --git a/cli/src/commands/__tests__/new.test.ts b/cli/src/commands/__tests__/new.test.ts index 7b6de9f24c7..c44e839c10f 100644 --- a/cli/src/commands/__tests__/new.test.ts +++ b/cli/src/commands/__tests__/new.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { newCommand } from "../new.js" import type { CommandContext } from "../core/types.js" +import { createMockContext } from "./helpers/mockContext.js" describe("/new command", () => { let mockContext: CommandContext @@ -12,44 +13,9 @@ describe("/new command", () => { // Mock process.stdout.write to capture terminal clearing vi.spyOn(process.stdout, "write").mockImplementation(() => true) - mockContext = { + mockContext = createMockContext({ input: "/new", - args: [], - options: {}, - sendMessage: vi.fn().mockResolvedValue(undefined), - addMessage: vi.fn(), - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - setMessageCutoffTimestamp: vi.fn(), - clearTask: vi.fn().mockResolvedValue(undefined), - setMode: vi.fn(), - exit: vi.fn(), - routerModels: null, - currentProvider: null, - kilocodeDefaultModel: "", - updateProviderModel: vi.fn().mockResolvedValue(undefined), - refreshRouterModels: vi.fn().mockResolvedValue(undefined), - updateProvider: vi.fn().mockResolvedValue(undefined), - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - refreshTerminal: vi.fn().mockResolvedValue(undefined), - taskHistoryData: null, - taskHistoryFilters: { - workspace: "current", - sort: "newest", - favoritesOnly: false, - }, - taskHistoryLoading: false, - taskHistoryError: null, - fetchTaskHistory: vi.fn().mockResolvedValue(undefined), - updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), - changeTaskHistoryPage: vi.fn().mockResolvedValue(null), - nextTaskHistoryPage: vi.fn().mockResolvedValue(null), - previousTaskHistoryPage: vi.fn().mockResolvedValue(null), - sendWebviewMessage: vi.fn().mockResolvedValue(undefined), - } + }) }) describe("Command metadata", () => { @@ -93,7 +59,7 @@ describe("/new command", () => { await newCommand.handler(mockContext) expect(mockContext.replaceMessages).toHaveBeenCalledTimes(1) - const replacedMessages = (mockContext.replaceMessages as any).mock.calls[0][0] + const replacedMessages = (mockContext.replaceMessages as ReturnType).mock.calls[0][0] expect(replacedMessages).toHaveLength(1) expect(replacedMessages[0]).toMatchObject({ @@ -160,7 +126,7 @@ describe("/new command", () => { expect(mockContext.replaceMessages).toHaveBeenCalled() // Verify welcome message was replaced - const replacedMessages = (mockContext.replaceMessages as any).mock.calls[0][0] + const replacedMessages = (mockContext.replaceMessages as ReturnType).mock.calls[0][0] expect(replacedMessages).toHaveLength(1) expect(replacedMessages[0].type).toBe("welcome") expect(replacedMessages[0].metadata?.welcomeOptions?.showInstructions).toBe(true) diff --git a/cli/src/commands/__tests__/profile.test.ts b/cli/src/commands/__tests__/profile.test.ts index 651dea97cf5..92a9df7db57 100644 --- a/cli/src/commands/__tests__/profile.test.ts +++ b/cli/src/commands/__tests__/profile.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { profileCommand } from "../profile.js" import type { CommandContext } from "../core/types.js" +import { createMockContext } from "./helpers/mockContext.js" describe("/profile command", () => { let mockContext: CommandContext @@ -13,32 +14,16 @@ describe("/profile command", () => { beforeEach(() => { addMessageMock = vi.fn() - mockContext = { + mockContext = createMockContext({ input: "/profile", - args: [], - options: {}, - sendMessage: vi.fn().mockResolvedValue(undefined), addMessage: addMessageMock, - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - clearTask: vi.fn().mockResolvedValue(undefined), - setMode: vi.fn(), - exit: vi.fn(), - routerModels: null, currentProvider: { id: "test-provider", provider: "kilocode", kilocodeToken: "test-token", }, kilocodeDefaultModel: "test-model", - updateProviderModel: vi.fn().mockResolvedValue(undefined), - refreshRouterModels: vi.fn().mockResolvedValue(undefined), - updateProvider: vi.fn().mockResolvedValue(undefined), - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - } + }) }) describe("Command metadata", () => { @@ -141,14 +126,16 @@ describe("/profile command", () => { await profileCommand.handler(mockContext) - const profileMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Profile Information"), - ) + const profileMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Profile Information") + }) expect(profileMessage).toBeDefined() if (profileMessage) { - expect(profileMessage[0].content).toContain("John Doe") - expect(profileMessage[0].content).toContain("john@example.com") - expect(profileMessage[0].content).toContain("$42.75") + const msg = profileMessage[0] as { content: string } + expect(msg.content).toContain("John Doe") + expect(msg.content).toContain("john@example.com") + expect(msg.content).toContain("$42.75") } }) @@ -173,12 +160,14 @@ describe("/profile command", () => { await profileCommand.handler(mockContext) - const profileMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Profile Information"), - ) + const profileMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Profile Information") + }) expect(profileMessage).toBeDefined() if (profileMessage) { - expect(profileMessage[0].content).toContain("Teams: Personal") + const msg = profileMessage[0] as { content: string } + expect(msg.content).toContain("Teams: Personal") } }) @@ -210,12 +199,14 @@ describe("/profile command", () => { await profileCommand.handler(mockContext) - const profileMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Profile Information"), - ) + const profileMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Profile Information") + }) expect(profileMessage).toBeDefined() if (profileMessage) { - expect(profileMessage[0].content).toContain("Teams: Acme Corp (admin)") + const msg = profileMessage[0] as { content: string } + expect(msg.content).toContain("Teams: Acme Corp (admin)") } }) }) @@ -238,9 +229,10 @@ describe("/profile command", () => { await profileCommand.handler(mockContext) - const errorMessage = addMessageMock.mock.calls.find( - (call: any) => call[0].type === "error" && call[0].content?.includes("No user data available"), - ) + const errorMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { type: string; content?: string } + return msg.type === "error" && msg.content?.includes("No user data available") + }) expect(errorMessage).toBeDefined() }) }) diff --git a/cli/src/commands/__tests__/teams.test.ts b/cli/src/commands/__tests__/teams.test.ts index 8aa215ced0e..71b9c8f79d1 100644 --- a/cli/src/commands/__tests__/teams.test.ts +++ b/cli/src/commands/__tests__/teams.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { teamsCommand } from "../teams.js" import type { CommandContext } from "../core/types.js" +import { createMockContext } from "./helpers/mockContext.js" describe("/teams command", () => { let mockContext: CommandContext @@ -15,32 +16,17 @@ describe("/teams command", () => { addMessageMock = vi.fn() updateProviderMock = vi.fn().mockResolvedValue(undefined) - mockContext = { + mockContext = createMockContext({ input: "/teams", - args: [], - options: {}, - sendMessage: vi.fn().mockResolvedValue(undefined), addMessage: addMessageMock, - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - clearTask: vi.fn().mockResolvedValue(undefined), - setMode: vi.fn(), - exit: vi.fn(), - routerModels: null, currentProvider: { id: "test-provider", provider: "kilocode", kilocodeToken: "test-token", }, kilocodeDefaultModel: "test-model", - updateProviderModel: vi.fn().mockResolvedValue(undefined), - refreshRouterModels: vi.fn().mockResolvedValue(undefined), updateProvider: updateProviderMock, - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - } + }) }) describe("Command metadata", () => { @@ -214,12 +200,14 @@ describe("/teams command", () => { kilocodeOrganizationId: undefined, }) - const successMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Switched to"), - ) + const successMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Switched to") + }) expect(successMessage).toBeDefined() if (successMessage) { - expect(successMessage[0].content).toContain("Personal") + const msg = successMessage[0] as { content: string } + expect(msg.content).toContain("Personal") } }) @@ -247,12 +235,14 @@ describe("/teams command", () => { kilocodeOrganizationId: "org-123", }) - const successMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Switched to team"), - ) + const successMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Switched to team") + }) expect(successMessage).toBeDefined() if (successMessage) { - expect(successMessage[0].content).toContain("Target Team") + const msg = successMessage[0] as { content: string } + expect(msg.content).toContain("Target Team") } }) @@ -280,12 +270,14 @@ describe("/teams command", () => { kilocodeOrganizationId: "org-456", }) - const successMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Switched to team"), - ) + const successMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Switched to team") + }) expect(successMessage).toBeDefined() if (successMessage) { - expect(successMessage[0].content).toContain("Kilo Code") + const msg = successMessage[0] as { content: string } + expect(msg.content).toContain("Kilo Code") } }) @@ -313,12 +305,14 @@ describe("/teams command", () => { kilocodeOrganizationId: "org-789", }) - const successMessage = addMessageMock.mock.calls.find((call: any) => - call[0].content?.includes("Switched to team"), - ) + const successMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { content?: string } + return msg.content?.includes("Switched to team") + }) expect(successMessage).toBeDefined() if (successMessage) { - expect(successMessage[0].content).toContain("My Awesome Team!") + const msg = successMessage[0] as { content: string } + expect(msg.content).toContain("My Awesome Team!") } }) @@ -336,9 +330,10 @@ describe("/teams command", () => { await teamsCommand.handler(mockContext) - const errorMessage = addMessageMock.mock.calls.find( - (call: any) => call[0].type === "error" && call[0].content?.includes("not found"), - ) + const errorMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { type: string; content?: string } + return msg.type === "error" && msg.content?.includes("not found") + }) expect(errorMessage).toBeDefined() }) @@ -371,9 +366,10 @@ describe("/teams command", () => { await teamsCommand.handler(mockContext) - const errorMessage = addMessageMock.mock.calls.find( - (call: any) => call[0].type === "error" && call[0].content?.includes("Failed to switch team"), - ) + const errorMessage = addMessageMock.mock.calls.find((call: unknown[]) => { + const msg = call[0] as { type: string; content?: string } + return msg.type === "error" && msg.content?.includes("Failed to switch team") + }) expect(errorMessage).toBeDefined() }) }) diff --git a/cli/src/commands/__tests__/theme.test.ts b/cli/src/commands/__tests__/theme.test.ts index 9efb1412b3a..2c1c15d935e 100644 --- a/cli/src/commands/__tests__/theme.test.ts +++ b/cli/src/commands/__tests__/theme.test.ts @@ -7,6 +7,7 @@ import { themeCommand } from "../theme.js" import type { CommandContext } from "../core/types.js" import type { Theme } from "../../types/theme.js" import type { CLIConfig } from "../../config/types.js" +import { createMockContext } from "./helpers/mockContext.js" // Mock the generateMessage utility vi.mock("../../ui/utils/messages.js", () => ({ @@ -249,52 +250,14 @@ describe("/theme command", () => { getBuiltinThemeIds: vi.fn(() => ["alpha", "dark", "dracula", "github-dark", "light", "github-light"]), })) - mockContext = { + mockContext = createMockContext({ input: "/theme", - args: [], - options: {}, config: mockConfig, - sendMessage: vi.fn().mockResolvedValue(undefined), addMessage: addMessageMock, - clearMessages: vi.fn(), - replaceMessages: vi.fn(), - setMessageCutoffTimestamp: vi.fn(), - clearTask: vi.fn().mockResolvedValue(undefined), - setMode: vi.fn(), - exit: vi.fn(), setTheme: setThemeMock, - setCommittingParallelMode: vi.fn(), - isParallelMode: false, refreshTerminal: refreshTerminalMock, - // Model-related context - routerModels: null, - currentProvider: null, kilocodeDefaultModel: "default-model", - updateProviderModel: vi.fn().mockResolvedValue(undefined), - refreshRouterModels: vi.fn().mockResolvedValue(undefined), - // Provider update function for teams command - updateProvider: vi.fn().mockResolvedValue(undefined), - // Profile data context - profileData: null, - balanceData: null, - profileLoading: false, - balanceLoading: false, - // Task history context - taskHistoryData: null, - taskHistoryFilters: { - workspace: "current", - sort: "newest", - favoritesOnly: false, - }, - taskHistoryLoading: false, - taskHistoryError: null, - fetchTaskHistory: vi.fn().mockResolvedValue(undefined), - updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), - changeTaskHistoryPage: vi.fn().mockResolvedValue(null), - nextTaskHistoryPage: vi.fn().mockResolvedValue(null), - previousTaskHistoryPage: vi.fn().mockResolvedValue(null), - sendWebviewMessage: vi.fn().mockResolvedValue(undefined), - } + }) }) describe("Command metadata", () => { diff --git a/cli/src/commands/checkpoint.ts b/cli/src/commands/checkpoint.ts index 53138f57404..37a0ccd3e4c 100644 --- a/cli/src/commands/checkpoint.ts +++ b/cli/src/commands/checkpoint.ts @@ -9,11 +9,11 @@ import { ExtensionMessage } from "../types/messages.js" /** * Interface for checkpoint message from chatMessages */ -interface CheckpointMessage { +interface CheckpointMessage extends ExtensionMessage { ts: number type: "say" - say: string - text?: string + say: "checkpoint_saved" + text: string metadata?: { type?: string fromHash?: string @@ -27,7 +27,13 @@ interface CheckpointMessage { */ function getCheckpointMessages(chatMessages: ExtensionMessage[]): CheckpointMessage[] { return chatMessages - .filter((msg): msg is CheckpointMessage => msg.type === "say" && msg.say === "checkpoint_saved" && !!msg.text) + .filter( + (msg): msg is CheckpointMessage => + msg.type === "say" && + msg.say === "checkpoint_saved" && + typeof msg.text === "string" && + msg.text.length > 0, + ) .reverse() // Most recent first } @@ -122,7 +128,7 @@ async function handleList(context: CommandContext): Promise { * Handle /checkpoint restore */ async function handleRestore(context: CommandContext, hash: string): Promise { - const { chatMessages, addMessage, sendMessage } = context + const { chatMessages, addMessage, sendWebviewMessage } = context const checkpoints = getCheckpointMessages(chatMessages) logs.debug("Finding checkpoint for restore", "checkpoint", { hash, checkpointCount: checkpoints.length }) @@ -160,7 +166,7 @@ async function handleRestore(context: CommandContext, hash: string): Promise = {} + const options: Record = {} const args: string[] = [] for (let i = 0; i < rest.length; i++) { @@ -131,7 +131,7 @@ function tokenize(input: string): string[] { /** * Parse a value string into its appropriate type */ -function parseValue(value: string): any { +function parseValue(value: string): string | number | boolean { // Try to parse as number if (/^-?\d+$/.test(value)) { return parseInt(value, 10) diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index 6cebda02745..eec03b505bc 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -2,7 +2,8 @@ * Command system type definitions */ -import type { ExtensionMessage, RouterModels } from "../../types/messages.js" +import type { ExtensionMessage, RouterModels, WebviewMessage } from "../../types/messages.js" +import type { CliMessage } from "../../types/cli.js" import type { CLIConfig, ProviderConfig } from "../../config/types.js" import type { ProfileData, BalanceData } from "../../state/atoms/profile.js" import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js" @@ -26,18 +27,18 @@ export interface CommandOption { description: string required?: boolean type: "string" | "number" | "boolean" - default?: any + default?: string | number | boolean } export interface CommandContext { input: string args: string[] - options: Record + options: Record config: CLIConfig - sendMessage: (message: any) => Promise - addMessage: (message: any) => void + sendMessage: (message: CliMessage) => Promise + addMessage: (message: CliMessage) => void clearMessages: () => void - replaceMessages: (messages: any[]) => void + replaceMessages: (messages: CliMessage[]) => void setMessageCutoffTimestamp: (timestamp: number) => void clearTask: () => Promise setMode: (mode: string) => void @@ -68,7 +69,7 @@ export interface CommandContext { changeTaskHistoryPage: (pageIndex: number) => Promise nextTaskHistoryPage: () => Promise previousTaskHistoryPage: () => Promise - sendWebviewMessage: (message: any) => Promise + sendWebviewMessage: (message: WebviewMessage) => Promise refreshTerminal: () => Promise chatMessages: ExtensionMessage[] } @@ -78,7 +79,7 @@ export type CommandHandler = (context: CommandContext) => Promise | void export interface ParsedCommand { command: string args: string[] - options: Record + options: Record } // Argument autocompletion types @@ -96,6 +97,19 @@ export interface ArgumentSuggestion { error?: string } +export interface ArgumentProviderCommandContext { + config: CLIConfig + routerModels: RouterModels | null + currentProvider: ProviderConfig | null + kilocodeDefaultModel: string + profileData: ProfileData | null + profileLoading: boolean + updateProviderModel: (modelId: string) => Promise + refreshRouterModels: () => Promise + taskHistoryData: TaskHistoryData | null + chatMessages: ExtensionMessage[] +} + /** * Context provided to argument providers */ @@ -107,7 +121,7 @@ export interface ArgumentProviderContext { // Current state currentArgs: string[] - currentOptions: Record + currentOptions: Record partialInput: string // Access to previous arguments by name @@ -116,25 +130,14 @@ export interface ArgumentProviderContext { // Access to all parsed values parsedValues: { args: Record - options: Record + options: Record } // Metadata about the command command: Command // CommandContext properties for providers that need them - commandContext?: { - config: CLIConfig - routerModels: RouterModels | null - currentProvider: ProviderConfig | null - kilocodeDefaultModel: string - profileData: ProfileData | null - profileLoading: boolean - updateProviderModel: (modelId: string) => Promise - refreshRouterModels: () => Promise - taskHistoryData: TaskHistoryData | null - chatMessages: ExtensionMessage[] - } + commandContext?: ArgumentProviderCommandContext } /** diff --git a/cli/src/commands/model.ts b/cli/src/commands/model.ts index 0d008634682..42cf2a38309 100644 --- a/cli/src/commands/model.ts +++ b/cli/src/commands/model.ts @@ -2,7 +2,8 @@ * /model command - View and manage AI models */ -import type { Command, ArgumentProviderContext } from "./core/types.js" +import type { Command, ArgumentProviderContext, CommandContext } from "./core/types.js" +import type { ModelInfo } from "../constants/providers/models.js" import { getModelsByProvider, getCurrentModelId, @@ -16,7 +17,7 @@ import { /** * Ensure router models are loaded for the current provider */ -async function ensureRouterModels(context: any): Promise { +async function ensureRouterModels(context: CommandContext): Promise { const { currentProvider, routerModels, refreshRouterModels, addMessage } = context if (!currentProvider) { @@ -75,7 +76,7 @@ async function ensureRouterModels(context: any): Promise { /** * Show current model information */ -async function showCurrentModel(context: any): Promise { +async function showCurrentModel(context: CommandContext): Promise { const { currentProvider, routerModels, kilocodeDefaultModel, addMessage } = context if (!currentProvider) { @@ -150,7 +151,7 @@ async function showCurrentModel(context: any): Promise { /** * Show detailed model information */ -async function showModelInfo(context: any, modelId: string): Promise { +async function showModelInfo(context: CommandContext, modelId: string): Promise { const { currentProvider, routerModels, kilocodeDefaultModel, addMessage } = context if (!currentProvider) { @@ -249,7 +250,7 @@ async function showModelInfo(context: any, modelId: string): Promise { /** * Select a different model */ -async function selectModel(context: any, modelId: string): Promise { +async function selectModel(context: CommandContext, modelId: string): Promise { const { currentProvider, routerModels, kilocodeDefaultModel, updateProviderModel, addMessage } = context if (!currentProvider) { @@ -317,7 +318,7 @@ async function selectModel(context: any, modelId: string): Promise { /** * List all available models */ -async function listModels(context: any, filter?: string): Promise { +async function listModels(context: CommandContext, filter?: string): Promise { const { currentProvider, routerModels, kilocodeDefaultModel, addMessage } = context if (!currentProvider) { @@ -352,10 +353,13 @@ async function listModels(context: any, filter?: string): Promise { modelIds = sortModelsByPreference( modelIds.reduce( (acc, id) => { - acc[id] = models[id] + const model = models[id] + if (model) { + acc[id] = model + } return acc }, - {} as Record, + {} as Record, ), ) diff --git a/cli/src/commands/profile.ts b/cli/src/commands/profile.ts index ed62f069598..b013555ca3c 100644 --- a/cli/src/commands/profile.ts +++ b/cli/src/commands/profile.ts @@ -2,13 +2,13 @@ * /profile command - View user profile information */ -import type { Command } from "./core/types.js" +import type { Command, CommandContext } from "./core/types.js" import type { UserOrganization } from "../state/atoms/profile.js" /** * Show user profile information */ -async function showProfile(context: any): Promise { +async function showProfile(context: CommandContext): Promise { const { currentProvider, addMessage, profileData, balanceData, profileLoading, balanceLoading } = context // Check if user is authenticated with Kilocode @@ -44,6 +44,16 @@ async function showProfile(context: any): Promise { } // Display profile information + if (!profileData) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "No profile data available", + ts: Date.now(), + }) + return + } + const user = profileData.user if (!user) { @@ -73,7 +83,7 @@ async function showProfile(context: any): Promise { // Show current organization if set const currentOrgId = currentProvider.kilocodeOrganizationId - if (currentOrgId && profileData.organizations) { + if (currentOrgId && profileData?.organizations) { const currentOrg = profileData.organizations.find((org: UserOrganization) => org.id === currentOrgId) if (currentOrg) { content += `Teams: ${currentOrg.name} (${currentOrg.role})\n` diff --git a/cli/src/commands/tasks.ts b/cli/src/commands/tasks.ts index 79f0a66fa04..2854b20cd91 100644 --- a/cli/src/commands/tasks.ts +++ b/cli/src/commands/tasks.ts @@ -3,8 +3,9 @@ */ import { generateMessage } from "../ui/utils/messages.js" -import type { Command, ArgumentProviderContext } from "./core/types.js" +import type { Command, ArgumentProviderContext, CommandContext } from "./core/types.js" import type { HistoryItem } from "@roo-code/types" +import type { TaskHistoryData, TaskHistoryFilters } from "../state/atoms/taskHistory.js" /** * Map kebab-case sort options to camelCase @@ -67,7 +68,7 @@ function truncate(text: string, maxLength: number): string { /** * Show current task history */ -async function showTaskHistory(context: any, dataOverride?: any): Promise { +async function showTaskHistory(context: CommandContext, dataOverride?: TaskHistoryData): Promise { const { taskHistoryData, taskHistoryLoading, taskHistoryError, fetchTaskHistory, addMessage } = context // Use override data if provided, otherwise use context data @@ -141,7 +142,7 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise /** * Search tasks */ -async function searchTasks(context: any, query: string): Promise { +async function searchTasks(context: CommandContext, query: string): Promise { const { updateTaskHistoryFilters, addMessage } = context if (!query) { @@ -176,7 +177,7 @@ async function searchTasks(context: any, query: string): Promise { /** * Select a task by ID */ -async function selectTask(context: any, taskId: string): Promise { +async function selectTask(context: CommandContext, taskId: string): Promise { const { sendWebviewMessage, addMessage, replaceMessages, refreshTerminal } = context if (!taskId) { @@ -223,7 +224,7 @@ async function selectTask(context: any, taskId: string): Promise { /** * Change page */ -async function changePage(context: any, pageNum: string): Promise { +async function changePage(context: CommandContext, pageNum: string): Promise { const { taskHistoryData, changeTaskHistoryPage, addMessage } = context if (!taskHistoryData) { @@ -269,7 +270,7 @@ async function changePage(context: any, pageNum: string): Promise { /** * Go to next page */ -async function nextPage(context: any): Promise { +async function nextPage(context: CommandContext): Promise { const { taskHistoryData, nextTaskHistoryPage, addMessage } = context if (!taskHistoryData) { addMessage({ @@ -312,7 +313,7 @@ async function nextPage(context: any): Promise { /** * Go to previous page */ -async function previousPage(context: any): Promise { +async function previousPage(context: CommandContext): Promise { const { taskHistoryData, previousTaskHistoryPage, addMessage } = context if (!taskHistoryData) { @@ -356,7 +357,7 @@ async function previousPage(context: any): Promise { /** * Change sort order */ -async function changeSortOrder(context: any, sortOption: string): Promise { +async function changeSortOrder(context: CommandContext, sortOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context const validSorts = Object.keys(SORT_OPTION_MAP) @@ -379,7 +380,7 @@ async function changeSortOrder(context: any, sortOption: string): Promise try { // Wait for the new data to arrive - const newData = await updateTaskHistoryFilters({ sort: mappedSort as any }) + const newData = await updateTaskHistoryFilters({ sort: mappedSort as TaskHistoryFilters["sort"] }) // Now display the fresh data await showTaskHistory(context, newData) } catch (error) { @@ -394,10 +395,10 @@ async function changeSortOrder(context: any, sortOption: string): Promise /** * Change filter */ -async function changeFilter(context: any, filterOption: string): Promise { +async function changeFilter(context: CommandContext, filterOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context - let filterUpdate: any + let filterUpdate: Partial let loadingMessage: string switch (filterOption) { diff --git a/cli/src/commands/teams.ts b/cli/src/commands/teams.ts index d2d6db8dae4..7d286eb3687 100644 --- a/cli/src/commands/teams.ts +++ b/cli/src/commands/teams.ts @@ -2,7 +2,7 @@ * /teams command - Manage team/organization selection */ -import type { Command, ArgumentProviderContext, ArgumentSuggestion } from "./core/types.js" +import type { Command, ArgumentProviderContext, ArgumentSuggestion, CommandContext } from "./core/types.js" import type { UserOrganization } from "../state/atoms/profile.js" /** @@ -22,7 +22,7 @@ function normalizeTeamName(name: string): string { /** * List all available teams */ -async function listTeams(context: any): Promise { +async function listTeams(context: CommandContext): Promise { const { currentProvider, addMessage, profileData, profileLoading } = context // Check if user is authenticated with Kilocode @@ -57,7 +57,7 @@ async function listTeams(context: any): Promise { return } - const organizations = profileData.organizations || [] + const organizations = profileData?.organizations || [] const currentOrgId = currentProvider.kilocodeOrganizationId if (organizations.length < 1) { @@ -81,7 +81,9 @@ async function listTeams(context: any): Promise { const isCurrent = org.id === currentOrgId content += `${isCurrent ? "→ " : " "}${normalizeTeamName(org.name)}${isCurrent ? " (current)" : ""}\n` } - content += `\nUse \`/teams select ${normalizeTeamName(organizations[0].name)}\` select the ${organizations[0].name} profile\n` + if (organizations.length > 0) { + content += `\nUse \`/teams select ${normalizeTeamName(organizations[0]!.name)}\` to select a team profile\n` + } content += `Use \`/teams select personal\` to switch to personal account\n` addMessage({ @@ -95,7 +97,7 @@ async function listTeams(context: any): Promise { /** * Select a team */ -async function selectTeam(context: any, teamId: string): Promise { +async function selectTeam(context: CommandContext, teamId: string): Promise { const { currentProvider, addMessage, updateProvider, profileData } = context // Check if user is authenticated with Kilocode @@ -198,7 +200,7 @@ async function teamAutocompleteProvider(context: ArgumentProviderContext): Promi return [] } - const { currentProvider, profileData, profileLoading } = context.commandContext as any + const { currentProvider, profileData, profileLoading } = context.commandContext // Check if user is authenticated with Kilocode if (!currentProvider || currentProvider.provider !== "kilocode") { diff --git a/cli/src/communication/ipc.ts b/cli/src/communication/ipc.ts index 62293edd1e0..1fd7aebdcca 100644 --- a/cli/src/communication/ipc.ts +++ b/cli/src/communication/ipc.ts @@ -5,7 +5,7 @@ import type { ExtensionMessage, WebviewMessage } from "../types/messages.js" export interface IPCMessage { id: string type: "request" | "response" | "event" - data: any + data: unknown ts: number } @@ -35,7 +35,7 @@ export class IPCChannel extends EventEmitter { /** * Send a request message and wait for response */ - async request(data: any): Promise { + async request(data: unknown): Promise { const id = this.generateMessageId() const message: IPCMessage = { id, @@ -63,7 +63,7 @@ export class IPCChannel extends EventEmitter { /** * Send a response to a request */ - respond(requestId: string, data: any): void { + respond(requestId: string, data: unknown): void { const message: IPCMessage = { id: requestId, type: "response", @@ -81,7 +81,7 @@ export class IPCChannel extends EventEmitter { /** * Send an event message (no response expected) */ - event(data: any): void { + event(data: unknown): void { const message: IPCMessage = { id: this.generateMessageId(), type: "event", @@ -201,7 +201,7 @@ export class MessageBridge extends EventEmitter { /** * Send a webview message from TUI to extension */ - async sendWebviewMessage(message: WebviewMessage): Promise { + async sendWebviewMessage(message: WebviewMessage): Promise { return this.tuiChannel.request({ type: "webviewMessage", payload: message, diff --git a/cli/src/config/__tests__/openConfig.test.ts b/cli/src/config/__tests__/openConfig.test.ts index 97575376b94..d7f69185846 100644 --- a/cli/src/config/__tests__/openConfig.test.ts +++ b/cli/src/config/__tests__/openConfig.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" -import { spawn } from "child_process" +import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from "vitest" +import { spawn, type ChildProcess } from "child_process" import { platform } from "os" import openConfigFile from "../openConfig.js" import * as configModule from "../index.js" @@ -19,9 +19,9 @@ vi.mock("../index.js", () => ({ describe("openConfigFile", () => { const mockConfigPath = "/home/user/.config/kilocode/config.json" let originalEnv: NodeJS.ProcessEnv - let consoleLogSpy: ReturnType - let consoleErrorSpy: ReturnType - let processExitSpy: any + let consoleLogSpy: MockInstance + let consoleErrorSpy: MockInstance + let processExitSpy: MockInstance beforeEach(() => { vi.clearAllMocks() @@ -35,7 +35,7 @@ describe("openConfigFile", () => { // Mock console methods consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}) consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) - processExitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any) + processExitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as never) }) afterEach(() => { @@ -43,9 +43,12 @@ describe("openConfigFile", () => { vi.restoreAllMocks() }) - const createMockProcess = () => { - const mockProcess = new EventEmitter() as any - mockProcess.stdio = { inherit: true } + const createMockProcess = (): ChildProcess => { + const mockProcess = new EventEmitter() as unknown as ChildProcess + Object.defineProperty(mockProcess, "stdio", { + value: { inherit: true }, + writable: true, + }) return mockProcess } diff --git a/cli/src/config/__tests__/persistence-merge.test.ts b/cli/src/config/__tests__/persistence-merge.test.ts index 8f5127aa36f..203ff5d53dd 100644 --- a/cli/src/config/__tests__/persistence-merge.test.ts +++ b/cli/src/config/__tests__/persistence-merge.test.ts @@ -10,7 +10,7 @@ vi.mock("fs/promises", async () => { const actual = await vi.importActual("fs/promises") return { ...actual, - readFile: vi.fn(async (filePath: any, encoding?: any) => { + readFile: vi.fn(async (filePath: string | Buffer | URL, encoding?: BufferEncoding | null) => { // If reading schema.json, return a minimal valid schema if (typeof filePath === "string" && filePath.includes("schema.json")) { return JSON.stringify({ @@ -20,7 +20,7 @@ vi.mock("fs/promises", async () => { }) } // Otherwise use the actual implementation - return actual.readFile(filePath, encoding) + return actual.readFile(filePath, encoding as BufferEncoding) }), } }) diff --git a/cli/src/config/__tests__/persistence.test.ts b/cli/src/config/__tests__/persistence.test.ts index 62c2495c872..a83960f0aa5 100644 --- a/cli/src/config/__tests__/persistence.test.ts +++ b/cli/src/config/__tests__/persistence.test.ts @@ -28,7 +28,7 @@ vi.mock("fs/promises", async () => { const actual = await vi.importActual("fs/promises") return { ...actual, - readFile: vi.fn(async (filePath: any, encoding?: any) => { + readFile: vi.fn(async (filePath: string | Buffer | URL, encoding?: BufferEncoding | null) => { // If reading schema.json, return a minimal valid schema if (typeof filePath === "string" && filePath.includes("schema.json")) { return JSON.stringify({ @@ -38,7 +38,7 @@ vi.mock("fs/promises", async () => { }) } // Otherwise use the actual implementation - return actual.readFile(filePath, encoding) + return actual.readFile(filePath, encoding as BufferEncoding) }), } }) diff --git a/cli/src/config/mapper.ts b/cli/src/config/mapper.ts index 29fd612f164..24f5623cd48 100644 --- a/cli/src/config/mapper.ts +++ b/cli/src/config/mapper.ts @@ -51,7 +51,8 @@ function mapProviderToApiConfig(provider: ProviderConfig): ProviderSettings { // Copy all provider-specific fields Object.keys(provider).forEach((key) => { if (key !== "id" && key !== "provider") { - config[key] = provider[key] + // Type assertion needed because we're dynamically accessing keys + ;(config as Record)[key] = (provider as Record)[key] } }) @@ -90,8 +91,41 @@ function getModelIdForProvider(provider: ProviderConfig): string { return provider.ioIntelligenceModelId || "" case "ovhcloud": return provider.ovhCloudAiEndpointsModelId || "" - default: - return provider.apiModelId || provider.modelId || "" + case "inception": + return provider.inceptionLabsModelId || "" + case "bedrock": + case "vertex": + case "gemini": + case "gemini-cli": + case "mistral": + case "moonshot": + case "minimax": + case "deepseek": + case "doubao": + case "qwen-code": + case "xai": + case "groq": + case "chutes": + case "cerebras": + case "sambanova": + case "zai": + case "fireworks": + case "featherless": + case "roo": + case "claude-code": + case "synthetic": + case "virtual-quota-fallback": + return provider.apiModelId || "" + case "vscode-lm": + if (provider.vsCodeLmModelSelector) { + return `${provider.vsCodeLmModelSelector.vendor}/${provider.vsCodeLmModelSelector.family}` + } + return "" + case "huggingface": + return provider.huggingFaceModelId || "" + case "human-relay": + case "fake-ai": + return "" } } @@ -111,11 +145,11 @@ export function mapExtensionStateToConfig(state: ExtensionState, currentConfig?: const existingProvider = config.providers.find((p) => p.id === providerId) if (!existingProvider) { - const newProvider: ProviderConfig = { + const newProvider = { id: providerId, provider: state.apiConfiguration.apiProvider || "kilocode", ...state.apiConfiguration, - } + } as ProviderConfig config.providers.push(newProvider) } else { // Update existing provider diff --git a/cli/src/config/types.ts b/cli/src/config/types.ts index 55afcf1424a..d5c948206fa 100644 --- a/cli/src/config/types.ts +++ b/cli/src/config/types.ts @@ -1,4 +1,3 @@ -import type { ProviderName } from "../types/messages.js" import type { ThemeId, Theme } from "../types/theme.js" /** @@ -106,13 +105,275 @@ export interface CLIConfig { customThemes?: Record } -export interface ProviderConfig { +// Base provider config with common fields +interface BaseProviderConfig { id: string - provider: ProviderName - // Provider-specific fields - [key: string]: any + [key: string]: unknown // Allow additional fields for flexibility } +// Provider-specific configurations with discriminated unions +type KilocodeProviderConfig = BaseProviderConfig & { + provider: "kilocode" + kilocodeModel?: string + kilocodeToken?: string + kilocodeOrganizationId?: string +} + +type AnthropicProviderConfig = BaseProviderConfig & { + provider: "anthropic" + apiModelId?: string +} + +type OpenAINativeProviderConfig = BaseProviderConfig & { + provider: "openai-native" + apiModelId?: string +} + +type OpenAIProviderConfig = BaseProviderConfig & { + provider: "openai" + apiModelId?: string +} + +type OpenRouterProviderConfig = BaseProviderConfig & { + provider: "openrouter" + openRouterModelId?: string +} + +type OllamaProviderConfig = BaseProviderConfig & { + provider: "ollama" + ollamaModelId?: string +} + +type LMStudioProviderConfig = BaseProviderConfig & { + provider: "lmstudio" + lmStudioModelId?: string +} + +type GlamaProviderConfig = BaseProviderConfig & { + provider: "glama" + glamaModelId?: string +} + +type LiteLLMProviderConfig = BaseProviderConfig & { + provider: "litellm" + litellmModelId?: string +} + +type DeepInfraProviderConfig = BaseProviderConfig & { + provider: "deepinfra" + deepInfraModelId?: string +} + +type UnboundProviderConfig = BaseProviderConfig & { + provider: "unbound" + unboundModelId?: string +} + +type RequestyProviderConfig = BaseProviderConfig & { + provider: "requesty" + requestyModelId?: string +} + +type VercelAiGatewayProviderConfig = BaseProviderConfig & { + provider: "vercel-ai-gateway" + vercelAiGatewayModelId?: string +} + +type IOIntelligenceProviderConfig = BaseProviderConfig & { + provider: "io-intelligence" + ioIntelligenceModelId?: string +} + +type OVHCloudProviderConfig = BaseProviderConfig & { + provider: "ovhcloud" + ovhCloudAiEndpointsModelId?: string +} + +type InceptionProviderConfig = BaseProviderConfig & { + provider: "inception" + inceptionLabsModelId?: string +} + +type BedrockProviderConfig = BaseProviderConfig & { + provider: "bedrock" + apiModelId?: string +} + +type VertexProviderConfig = BaseProviderConfig & { + provider: "vertex" + apiModelId?: string +} + +type GeminiProviderConfig = BaseProviderConfig & { + provider: "gemini" + apiModelId?: string +} + +type GeminiCliProviderConfig = BaseProviderConfig & { + provider: "gemini-cli" + apiModelId?: string +} + +type MistralProviderConfig = BaseProviderConfig & { + provider: "mistral" + apiModelId?: string +} + +type MoonshotProviderConfig = BaseProviderConfig & { + provider: "moonshot" + apiModelId?: string +} + +type MinimaxProviderConfig = BaseProviderConfig & { + provider: "minimax" + apiModelId?: string +} + +type DeepSeekProviderConfig = BaseProviderConfig & { + provider: "deepseek" + apiModelId?: string +} + +type DoubaoProviderConfig = BaseProviderConfig & { + provider: "doubao" + apiModelId?: string +} + +type QwenCodeProviderConfig = BaseProviderConfig & { + provider: "qwen-code" + apiModelId?: string +} + +type XAIProviderConfig = BaseProviderConfig & { + provider: "xai" + apiModelId?: string +} + +type GroqProviderConfig = BaseProviderConfig & { + provider: "groq" + apiModelId?: string +} + +type ChutesProviderConfig = BaseProviderConfig & { + provider: "chutes" + apiModelId?: string +} + +type CerebrasProviderConfig = BaseProviderConfig & { + provider: "cerebras" + apiModelId?: string +} + +type SambaNovaProviderConfig = BaseProviderConfig & { + provider: "sambanova" + apiModelId?: string +} + +type ZAIProviderConfig = BaseProviderConfig & { + provider: "zai" + apiModelId?: string +} + +type FireworksProviderConfig = BaseProviderConfig & { + provider: "fireworks" + apiModelId?: string +} + +type FeatherlessProviderConfig = BaseProviderConfig & { + provider: "featherless" + apiModelId?: string +} + +type RooProviderConfig = BaseProviderConfig & { + provider: "roo" + apiModelId?: string +} + +type ClaudeCodeProviderConfig = BaseProviderConfig & { + provider: "claude-code" + apiModelId?: string +} + +type VSCodeLMProviderConfig = BaseProviderConfig & { + provider: "vscode-lm" + vsCodeLmModelSelector?: { + vendor?: string + family?: string + version?: string + id?: string + } +} + +type HuggingFaceProviderConfig = BaseProviderConfig & { + provider: "huggingface" + huggingFaceModelId?: string +} + +type SyntheticProviderConfig = BaseProviderConfig & { + provider: "synthetic" + apiModelId?: string +} + +type VirtualQuotaFallbackProviderConfig = BaseProviderConfig & { + provider: "virtual-quota-fallback" + apiModelId?: string +} + +type HumanRelayProviderConfig = BaseProviderConfig & { + provider: "human-relay" + // No model ID field +} + +type FakeAIProviderConfig = BaseProviderConfig & { + provider: "fake-ai" + // No model ID field +} + +// Discriminated union of all provider configs +export type ProviderConfig = + | KilocodeProviderConfig + | AnthropicProviderConfig + | OpenAINativeProviderConfig + | OpenAIProviderConfig + | OpenRouterProviderConfig + | OllamaProviderConfig + | LMStudioProviderConfig + | GlamaProviderConfig + | LiteLLMProviderConfig + | DeepInfraProviderConfig + | UnboundProviderConfig + | RequestyProviderConfig + | VercelAiGatewayProviderConfig + | IOIntelligenceProviderConfig + | OVHCloudProviderConfig + | InceptionProviderConfig + | BedrockProviderConfig + | VertexProviderConfig + | GeminiProviderConfig + | GeminiCliProviderConfig + | MistralProviderConfig + | MoonshotProviderConfig + | MinimaxProviderConfig + | DeepSeekProviderConfig + | DoubaoProviderConfig + | QwenCodeProviderConfig + | XAIProviderConfig + | GroqProviderConfig + | ChutesProviderConfig + | CerebrasProviderConfig + | SambaNovaProviderConfig + | ZAIProviderConfig + | FireworksProviderConfig + | FeatherlessProviderConfig + | RooProviderConfig + | ClaudeCodeProviderConfig + | VSCodeLMProviderConfig + | HuggingFaceProviderConfig + | SyntheticProviderConfig + | VirtualQuotaFallbackProviderConfig + | HumanRelayProviderConfig + | FakeAIProviderConfig + // Type guards export function isValidConfig(config: unknown): config is CLIConfig { return ( diff --git a/cli/src/config/validation.ts b/cli/src/config/validation.ts index ec2b9332fdb..226e4a66599 100644 --- a/cli/src/config/validation.ts +++ b/cli/src/config/validation.ts @@ -64,7 +64,8 @@ export async function validateConfig(config: unknown): Promise * Helper function to validate a required field */ function validateRequiredField(provider: ProviderConfig, fieldName: string, errors: string[]): void { - if (!provider[fieldName] || provider[fieldName].length === 0) { + const value = provider[fieldName] + if (!value || (typeof value === "string" && value.length === 0)) { errors.push(`${fieldName} is required and cannot be empty for selected provider`) } } @@ -74,10 +75,12 @@ function validateRequiredField(provider: ProviderConfig, fieldName: string, erro */ function handleSpecialValidations(provider: ProviderConfig, errors: string[]): void { switch (provider.provider) { - case "vertex": + case "vertex": { // At least one of vertexJsonCredentials or vertexKeyFile must be provided - const hasJsonCredentials = provider.vertexJsonCredentials && provider.vertexJsonCredentials.length > 0 - const hasKeyFile = provider.vertexKeyFile && provider.vertexKeyFile.length > 0 + const jsonCreds = provider.vertexJsonCredentials as string | undefined + const keyFile = provider.vertexKeyFile as string | undefined + const hasJsonCredentials = jsonCreds && jsonCreds.length > 0 + const hasKeyFile = keyFile && keyFile.length > 0 if (!hasJsonCredentials && !hasKeyFile) { errors.push( @@ -90,25 +93,30 @@ function handleSpecialValidations(provider: ProviderConfig, errors: string[]): v validateRequiredField(provider, "vertexRegion", errors) validateRequiredField(provider, "apiModelId", errors) break + } - case "vscode-lm": - if (!provider.vsCodeLmModelSelector) { + case "vscode-lm": { + const selector = provider.vsCodeLmModelSelector as { vendor?: string; family?: string } | undefined + if (!selector) { errors.push("vsCodeLmModelSelector is required for selected provider") } else { - if (!provider.vsCodeLmModelSelector.vendor || provider.vsCodeLmModelSelector.vendor.length === 0) { + if (!selector.vendor || selector.vendor.length === 0) { errors.push("vsCodeLmModelSelector.vendor is required and cannot be empty for selected provider") } - if (!provider.vsCodeLmModelSelector.family || provider.vsCodeLmModelSelector.family.length === 0) { + if (!selector.family || selector.family.length === 0) { errors.push("vsCodeLmModelSelector.family is required and cannot be empty for selected provider") } } break + } - case "virtual-quota-fallback": - if (!provider.profiles || !Array.isArray(provider.profiles) || provider.profiles.length === 0) { + case "virtual-quota-fallback": { + const profiles = provider.profiles as unknown[] | undefined + if (!profiles || !Array.isArray(profiles) || profiles.length === 0) { errors.push("profiles is required and must be a non-empty array for selected provider") } break + } } } diff --git a/cli/src/constants/modes/defaults.ts b/cli/src/constants/modes/defaults.ts index dd706bbb0d5..fe81a1a9ce4 100644 --- a/cli/src/constants/modes/defaults.ts +++ b/cli/src/constants/modes/defaults.ts @@ -1,45 +1,10 @@ import type { ModeConfig } from "../../types/messages.js" +import { DEFAULT_MODES as DEFAULT_MODES_KILO } from "@roo-code/types" /** - * Default mode configurations - * These are the built-in modes available in the application - */ -export const DEFAULT_MODES: ModeConfig[] = [ - { - slug: "architect", - name: "Architect", - description: "Plan and design system architecture", - source: "global", - }, - { - slug: "code", - name: "Code", - description: "Write, modify, and refactor code", - source: "global", - }, - { - slug: "ask", - name: "Ask", - description: "Get explanations and answers", - source: "global", - }, - { - slug: "debug", - name: "Debug", - description: "Troubleshoot and fix issues", - source: "global", - }, - { - slug: "orchestrator", - name: "Orchestrator", - description: "Coordinate complex multi-step projects", - source: "global", - }, -] - -/** - * Default mode slug + * Default mode */ +export const DEFAULT_MODES = DEFAULT_MODES_KILO export const DEFAULT_MODE_SLUG = "code" /** diff --git a/cli/src/constants/providers/__tests__/models.test.ts b/cli/src/constants/providers/__tests__/models.test.ts index f16007491b1..4e83346e89d 100644 --- a/cli/src/constants/providers/__tests__/models.test.ts +++ b/cli/src/constants/providers/__tests__/models.test.ts @@ -510,7 +510,7 @@ describe("Static Provider Models", () => { }) it("should handle null price", () => { - expect(formatPrice(null as any)).toBe("N/A") + expect(formatPrice(null as unknown as number)).toBe("N/A") }) }) @@ -617,7 +617,7 @@ describe("Static Provider Models", () => { kilocodeDefaultModel: "", }) - Object.entries(result.models).forEach(([modelId, model]) => { + Object.entries(result.models).forEach(([_modelId, model]) => { expect(model.contextWindow).toBeDefined() expect(typeof model.contextWindow).toBe("number") expect(model.contextWindow).toBeGreaterThan(0) @@ -634,7 +634,7 @@ describe("Static Provider Models", () => { kilocodeDefaultModel: "", }) - Object.entries(result.models).forEach(([modelId, model]) => { + Object.entries(result.models).forEach(([_modelId, model]) => { if (model.inputPrice !== undefined) { expect(typeof model.inputPrice).toBe("number") expect(model.inputPrice).toBeGreaterThanOrEqual(0) @@ -653,7 +653,7 @@ describe("Static Provider Models", () => { kilocodeDefaultModel: "", }) - Object.entries(result.models).forEach(([modelId, model]) => { + Object.entries(result.models).forEach(([_modelId, model]) => { if (model.maxTokens !== undefined && model.maxTokens !== null) { expect(typeof model.maxTokens).toBe("number") expect(model.maxTokens).toBeGreaterThan(0) diff --git a/cli/src/constants/providers/labels.ts b/cli/src/constants/providers/labels.ts index 81db652cd4d..89e57e85f1a 100644 --- a/cli/src/constants/providers/labels.ts +++ b/cli/src/constants/providers/labels.ts @@ -45,6 +45,8 @@ export const PROVIDER_LABELS: Record = { "human-relay": "Human Relay", "fake-ai": "Fake AI", ovhcloud: "OVHcloud AI Endpoints", + inception: "Inception", + synthetic: "Synthetic", } /** diff --git a/cli/src/constants/providers/models.ts b/cli/src/constants/providers/models.ts index 65ab366a610..ef0b3f75147 100644 --- a/cli/src/constants/providers/models.ts +++ b/cli/src/constants/providers/models.ts @@ -1,4 +1,4 @@ -import type { ProviderName } from "../../types/messages.js" +import type { ProviderName, ProviderSettings } from "../../types/messages.js" import type { ProviderConfig } from "../../config/types.js" // Import model definitions from @roo-code/types @@ -139,6 +139,8 @@ export const PROVIDER_TO_ROUTER_NAME: Record = "gemini-cli": null, "virtual-quota-fallback": null, huggingface: null, + inception: null, + synthetic: null, } /** @@ -186,6 +188,8 @@ export const PROVIDER_MODEL_FIELD: Record = { "gemini-cli": null, "virtual-quota-fallback": null, huggingface: null, + inception: "inceptionLabsModelId", + synthetic: null, } /** @@ -449,8 +453,8 @@ export function getCurrentModelId(params: { // Special handling for vscode-lm if (provider === "vscode-lm" && providerConfig.vsCodeLmModelSelector) { - const selector = providerConfig.vsCodeLmModelSelector as any - return `${selector.vendor}/${selector.family}` + const selector = providerConfig.vsCodeLmModelSelector as ProviderSettings["vsCodeLmModelSelector"] + return `${selector?.vendor}/${selector?.family}` } // Get model ID from config diff --git a/cli/src/constants/providers/settings.ts b/cli/src/constants/providers/settings.ts index 8b854409bac..3147f9898e0 100644 --- a/cli/src/constants/providers/settings.ts +++ b/cli/src/constants/providers/settings.ts @@ -550,7 +550,8 @@ export const isOptionalField = (field: string): boolean => { */ const createFieldConfig = (field: string, config: ProviderSettings, defaultValue?: string): ProviderSettingConfig => { const fieldInfo = getFieldInfo(field) - const actualValue = (config as any)[field] || "" + const rawValue = config[field as keyof ProviderSettings] + const actualValue = rawValue ?? "" let displayValue: string if (fieldInfo.type === "password") { @@ -558,14 +559,14 @@ const createFieldConfig = (field: string, config: ProviderSettings, defaultValue } else if (fieldInfo.type === "boolean") { displayValue = actualValue ? "Enabled" : "Disabled" } else { - displayValue = actualValue || defaultValue || "Not set" + displayValue = (typeof actualValue === "string" ? actualValue : "") || defaultValue || "Not set" } return { field, label: fieldInfo.label, value: displayValue, - actualValue: fieldInfo.type === "boolean" ? (actualValue ? "true" : "false") : actualValue, + actualValue: fieldInfo.type === "boolean" ? (actualValue ? "true" : "false") : String(actualValue), type: fieldInfo.type, } } @@ -862,6 +863,8 @@ export const PROVIDER_DEFAULT_MODELS: Record = { minimax: "MiniMax-M2", "fake-ai": "fake-model", ovhcloud: "gpt-oss-120b", + inception: "gpt-4o", + synthetic: "synthetic-model", } /** diff --git a/cli/src/constants/providers/validation.ts b/cli/src/constants/providers/validation.ts index 3d1d916a9a9..04e5df57690 100644 --- a/cli/src/constants/providers/validation.ts +++ b/cli/src/constants/providers/validation.ts @@ -40,7 +40,9 @@ export const PROVIDER_REQUIRED_FIELDS: Record = { "vercel-ai-gateway": ["vercelAiGatewayApiKey", "vercelAiGatewayModelId"], "human-relay": ["apiModelId"], "fake-ai": ["apiModelId"], - ovhcloud: ["ovhCloudAiEndpointsModelId"], + ovhcloud: ["ovhCloudAiEndpointsApiKey", "ovhCloudAiEndpointsModelId"], + inception: ["inceptionLabsApiKey", "inceptionLabsModelId"], + synthetic: ["syntheticApiKey", "apiModelId"], // Special cases handled separately in handleSpecialValidations vertex: [], // Has special validation logic (either/or fields) "vscode-lm": [], // Has nested object validation diff --git a/cli/src/debug/keyboard/KeyboardDebugUI.tsx b/cli/src/debug/keyboard/KeyboardDebugUI.tsx index 1adcfe9d275..be6885f6630 100644 --- a/cli/src/debug/keyboard/KeyboardDebugUI.tsx +++ b/cli/src/debug/keyboard/KeyboardDebugUI.tsx @@ -23,14 +23,22 @@ interface KeyboardDebugUIProps { */ function escapeSequence(str: string): string { return str - .replace(/\x1b/g, "\\x1b") - .replace(/\r/g, "\\r") - .replace(/\n/g, "\\n") - .replace(/\t/g, "\\t") .split("") .map((char) => { const code = char.charCodeAt(0) // Show non-printable characters as hex + if (code === 0x1b) { + return "\\x1b" + } + if (code === 0x0d) { + return "\\r" + } + if (code === 0x0a) { + return "\\n" + } + if (code === 0x09) { + return "\\t" + } if (code < 32 || code > 126) { return `\\x${code.toString(16).padStart(2, "0")}` } diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index 48258aff946..a4c2cfbfcf1 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "events" -import { createVSCodeAPIMock, type IdentityInfo } from "./VSCode.js" +import { createVSCodeAPIMock, type IdentityInfo, type ExtensionContext } from "./VSCode.js" import { logs } from "../services/logs.js" import type { ExtensionMessage, WebviewMessage, ExtensionState } from "../types/messages.js" import { getTelemetryService } from "../services/telemetry/index.js" @@ -11,6 +11,35 @@ export interface ExtensionHostOptions { identity?: IdentityInfo // Identity information for VSCode environment } +// Extension module interface +interface ExtensionModule { + activate: (context: unknown) => Promise | KiloCodeAPI + deactivate?: () => Promise | void +} + +// KiloCode API interface returned by extension activation +interface KiloCodeAPI { + startNewTask?: (task: string, images?: string[]) => Promise + sendMessage?: (message: ExtensionMessage) => void + cancelTask?: () => Promise + condense?: () => Promise + condenseTaskContext?: () => Promise + handleTerminalOperation?: (operation: string) => Promise + getState?: () => ExtensionState | Promise +} + +// VSCode API mock interface - matches the return type from createVSCodeAPIMock +interface VSCodeAPIMock { + context: ExtensionContext + [key: string]: unknown +} + +// Webview provider interface +interface WebviewProvider { + handleCLIMessage?: (message: WebviewMessage) => Promise + [key: string]: unknown +} + export interface ExtensionAPI { getState: () => ExtensionState | null sendMessage: (message: ExtensionMessage) => void @@ -21,10 +50,10 @@ export class ExtensionHost extends EventEmitter { private options: ExtensionHostOptions private isActivated = false private currentState: ExtensionState | null = null - private extensionModule: any = null - private extensionAPI: any = null - private vscodeAPI: any = null - private webviewProviders: Map = new Map() + private extensionModule: ExtensionModule | null = null + private extensionAPI: KiloCodeAPI | null = null + private vscodeAPI: VSCodeAPIMock | null = null + private webviewProviders: Map = new Map() private webviewInitialized = false private pendingMessages: WebviewMessage[] = [] private isInitialSetup = true @@ -43,7 +72,7 @@ export class ExtensionHost extends EventEmitter { lastErrorTime: 0, maxErrorsBeforeWarning: 10, } - private unhandledRejectionHandler: ((reason: any, promise: Promise) => void) | null = null + private unhandledRejectionHandler: ((reason: unknown, promise: Promise) => void) | null = null private uncaughtExceptionHandler: ((error: Error) => void) | null = null constructor(options: ExtensionHostOptions) { @@ -59,7 +88,7 @@ export class ExtensionHost extends EventEmitter { */ private setupGlobalErrorHandlers(): void { // Handle unhandled promise rejections from extension - this.unhandledRejectionHandler = (reason: any) => { + this.unhandledRejectionHandler = (reason: unknown) => { const error = reason instanceof Error ? reason : new Error(String(reason)) // Check if this is an expected error @@ -168,9 +197,9 @@ export class ExtensionHost extends EventEmitter { /** * Check if an error is expected (e.g., task abortion) */ - private isExpectedError(error: any): boolean { + private isExpectedError(error: unknown): boolean { if (!error) return false - const errorMessage = error.message || error.toString() + const errorMessage = error instanceof Error ? error.message : String(error) // Task abortion errors are expected if (errorMessage.includes("task") && errorMessage.includes("aborted")) { @@ -332,13 +361,15 @@ export class ExtensionHost extends EventEmitter { this.options.extensionRootPath, this.options.workspacePath, this.options.identity, - ) + ) as VSCodeAPIMock // Set global vscode object for the extension - ;(global as any).vscode = this.vscodeAPI + if (this.vscodeAPI) { + ;(global as unknown as { vscode: VSCodeAPIMock }).vscode = this.vscodeAPI + } // Set global reference to this ExtensionHost for webview provider registration - ;(global as any).__extensionHost = this + ;(global as unknown as { __extensionHost: ExtensionHost }).__extensionHost = this // Set environment variables to disable problematic features in CLI mode process.env.KILO_CLI_MODE = "true" @@ -359,27 +390,27 @@ export class ExtensionHost extends EventEmitter { // Override console methods to forward to LogsService ONLY (no console output) // IMPORTANT: Use original console methods to avoid circular dependency - console.log = (...args: any[]) => { + console.log = (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.info(message, "Extension") } - console.error = (...args: any[]) => { + console.error = (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.error(message, "Extension") } - console.warn = (...args: any[]) => { + console.warn = (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.warn(message, "Extension") } - console.debug = (...args: any[]) => { + console.debug = (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.debug(message, "Extension") } - console.info = (...args: any[]) => { + console.info = (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.info(message, "Extension") } @@ -396,8 +427,8 @@ export class ExtensionHost extends EventEmitter { } // Clean up global console interception - if ((global as any).__interceptedConsole) { - delete (global as any).__interceptedConsole + if ((global as unknown as { __interceptedConsole?: unknown }).__interceptedConsole) { + delete (global as unknown as { __interceptedConsole?: unknown }).__interceptedConsole } logs.debug("Console methods and streams restored", "ExtensionHost") @@ -416,14 +447,25 @@ export class ExtensionHost extends EventEmitter { // Get Module class for interception const Module = await import("module") - const ModuleClass = Module.default as any + interface ModuleClass { + _resolveFilename: (request: string, parent: unknown, isMain: boolean, options?: unknown) => string + prototype: { + _compile: (content: string, filename: string) => unknown + } + } + const ModuleClass = Module.default as unknown as ModuleClass // Store original methods const originalResolveFilename = ModuleClass._resolveFilename const originalCompile = ModuleClass.prototype._compile // Set up module resolution interception for vscode - ModuleClass._resolveFilename = function (request: string, parent: any, isMain: boolean, options?: any) { + ModuleClass._resolveFilename = function ( + request: string, + parent: unknown, + isMain: boolean, + options?: unknown, + ) { if (request === "vscode") { return "vscode-mock" } @@ -453,31 +495,31 @@ export class ExtensionHost extends EventEmitter { children: [], exports: this.vscodeAPI, paths: [], - } as any + } as unknown as NodeModule // Store the intercepted console in global for module injection - ;(global as any).__interceptedConsole = { - log: (...args: any[]) => { + ;(global as unknown as { __interceptedConsole: Console }).__interceptedConsole = { + log: (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.info(message, "Extension") }, - error: (...args: any[]) => { + error: (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.error(message, "Extension") }, - warn: (...args: any[]) => { + warn: (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.warn(message, "Extension") }, - debug: (...args: any[]) => { + debug: (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.debug(message, "Extension") }, - info: (...args: any[]) => { + info: (...args: unknown[]) => { const message = args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))).join(" ") logs.info(message, "Extension") }, - } + } as Console // Clear extension require cache to ensure fresh load if (require.cache[extensionPath]) { @@ -512,11 +554,17 @@ export class ExtensionHost extends EventEmitter { // Call the extension's activate function with our mocked context // Use safeExecute to catch and handle any errors without crashing the CLI - this.extensionAPI = await this.safeExecute( - async () => await this.extensionModule.activate(this.vscodeAPI.context), - "extension.activate", - null, - ) + this.extensionAPI = + (await this.safeExecute( + async () => { + if (!this.extensionModule || !this.vscodeAPI) { + throw new Error("Extension module or VSCode API not initialized") + } + return await this.extensionModule.activate(this.vscodeAPI.context) + }, + "extension.activate", + null, + )) ?? null if (!this.extensionAPI) { logs.warn( @@ -563,112 +611,144 @@ export class ExtensionHost extends EventEmitter { const processedMessageIds = new Set() // Listen for messages from the extension's webview (postMessage calls) - this.on("extensionWebviewMessage", (message: any) => { - this.safeExecute(() => { - // Create a unique ID for this message to prevent loops - const messageId = `${message.type}_${Date.now()}_${JSON.stringify(message).slice(0, 50)}` - - if (processedMessageIds.has(messageId)) { - logs.debug(`Skipping duplicate message: ${message.type}`, "ExtensionHost") - return - } + this.on( + "extensionWebviewMessage", + ( + message: ExtensionMessage & { + payload?: unknown + state?: Partial + clineMessage?: unknown + chatMessage?: unknown + listApiConfigMeta?: unknown + }, + ) => { + this.safeExecute(() => { + // Create a unique ID for this message to prevent loops + const messageId = `${message.type}_${Date.now()}_${JSON.stringify(message).slice(0, 50)}` + + if (processedMessageIds.has(messageId)) { + logs.debug(`Skipping duplicate message: ${message.type}`, "ExtensionHost") + return + } - processedMessageIds.add(messageId) + processedMessageIds.add(messageId) - // Clean up old message IDs to prevent memory leaks - if (processedMessageIds.size > 100) { - const oldestIds = Array.from(processedMessageIds).slice(0, 50) - oldestIds.forEach((id) => processedMessageIds.delete(id)) - } + // Clean up old message IDs to prevent memory leaks + if (processedMessageIds.size > 100) { + const oldestIds = Array.from(processedMessageIds).slice(0, 50) + oldestIds.forEach((id) => processedMessageIds.delete(id)) + } - // Track extension message received - getTelemetryService().trackExtensionMessageReceived(message.type) - - // Only forward specific message types that are important for CLI - switch (message.type) { - case "state": - // Extension is sending a full state update - if (message.state && this.currentState) { - this.currentState = { - ...this.currentState, - ...message.state, - chatMessages: - message.state.clineMessages || - message.state.chatMessages || - this.currentState.chatMessages, - apiConfiguration: - message.state.apiConfiguration || this.currentState.apiConfiguration, - currentApiConfigName: - message.state.currentApiConfigName || this.currentState.currentApiConfigName, - listApiConfigMeta: - message.state.listApiConfigMeta || this.currentState.listApiConfigMeta, - routerModels: message.state.routerModels || this.currentState.routerModels, + // Track extension message received + getTelemetryService().trackExtensionMessageReceived(message.type) + + // Only forward specific message types that are important for CLI + switch (message.type) { + case "state": + // Extension is sending a full state update + if (message.state && this.currentState) { + // Build the new state object, handling optional properties correctly + const newState: ExtensionState = { + ...this.currentState, + ...message.state, + chatMessages: + message.state.clineMessages || + message.state.chatMessages || + this.currentState.chatMessages, + apiConfiguration: + message.state.apiConfiguration || this.currentState.apiConfiguration, + } + + // Handle optional properties explicitly to satisfy exactOptionalPropertyTypes + if (message.state.currentApiConfigName !== undefined) { + newState.currentApiConfigName = message.state.currentApiConfigName + } else if (this.currentState.currentApiConfigName !== undefined) { + newState.currentApiConfigName = this.currentState.currentApiConfigName + } + + if (message.state.listApiConfigMeta !== undefined) { + newState.listApiConfigMeta = message.state.listApiConfigMeta + } else if (this.currentState.listApiConfigMeta !== undefined) { + newState.listApiConfigMeta = this.currentState.listApiConfigMeta + } + + if (message.state.routerModels !== undefined) { + newState.routerModels = message.state.routerModels + } else if (this.currentState.routerModels !== undefined) { + newState.routerModels = this.currentState.routerModels + } + + this.currentState = newState + + // Forward the updated state to the CLI + this.emit("message", { + type: "state", + state: this.currentState, + }) } - - // Forward the updated state to the CLI - this.emit("message", { - type: "state", - state: this.currentState, - }) - } - break - - case "messageUpdated": { - // Extension is sending an individual message update - // The extension uses 'clineMessage' property (legacy name) - - const chatMessage = message.clineMessage || message.chatMessage - if (chatMessage) { - // Forward the message update to the CLI - const emitMessage = { - type: "messageUpdated", - chatMessage: chatMessage, + break + + case "messageUpdated": { + // Extension is sending an individual message update + // The extension uses 'clineMessage' property (legacy name) + + const chatMessage = message.clineMessage || message.chatMessage + if (chatMessage) { + // Forward the message update to the CLI + const emitMessage = { + type: "messageUpdated", + chatMessage: chatMessage, + } + this.emit("message", emitMessage) } - this.emit("message", emitMessage) + break } - break - } - case "taskHistoryResponse": - // Extension is sending task history data - if (message.payload) { - // Forward the task history response to the CLI - this.emit("message", { - type: "taskHistoryResponse", - payload: message.payload, - }) - } - break - - // Handle configuration-related messages from extension - case "listApiConfig": - // Extension is sending updated API configuration list - if (message.listApiConfigMeta && this.currentState) { - this.currentState.listApiConfigMeta = message.listApiConfigMeta - logs.debug("Updated listApiConfigMeta from extension", "ExtensionHost") - } - break - - // Don't forward these message types as they can cause loops - case "mcpServers": - case "theme": - case "rulesData": - logs.debug( - `Ignoring extension message type to prevent loops: ${message.type}`, - "ExtensionHost", - ) - break - - default: - // Only forward other important messages - if (message.type && !message.type.startsWith("_")) { - logs.debug(`Forwarding extension message: ${message.type}`, "ExtensionHost") - this.emit("message", message) - } - break - } - }, `extensionWebviewMessage-${message.type}`) - }) + case "taskHistoryResponse": + // Extension is sending task history data + if (message.payload) { + // Forward the task history response to the CLI + this.emit("message", { + type: "taskHistoryResponse", + payload: message.payload, + }) + } + break + + // Handle configuration-related messages from extension + case "listApiConfig": + // Extension is sending updated API configuration list + if ( + message.listApiConfigMeta && + this.currentState && + Array.isArray(message.listApiConfigMeta) + ) { + this.currentState.listApiConfigMeta = message.listApiConfigMeta + logs.debug("Updated listApiConfigMeta from extension", "ExtensionHost") + } + break + + // Don't forward these message types as they can cause loops + case "mcpServers": + case "theme": + case "rulesData": + logs.debug( + `Ignoring extension message type to prevent loops: ${message.type}`, + "ExtensionHost", + ) + break + + default: + // Only forward other important messages + if (message.type && !message.type.startsWith("_")) { + logs.debug(`Forwarding extension message: ${message.type}`, "ExtensionHost") + this.emit("message", message) + } + break + } + }, `extensionWebviewMessage-${message.type}`) + }, + ) } } @@ -714,19 +794,38 @@ export class ExtensionHost extends EventEmitter { // Sync with extension state when webview launches if (this.extensionAPI && typeof this.extensionAPI.getState === "function") { try { - const extensionState = await this.safeExecute(() => this.extensionAPI.getState(), "getState", null) + const extensionState = await this.safeExecute( + () => { + if (!this.extensionAPI?.getState) { + return null + } + const result = this.extensionAPI.getState() + return result instanceof Promise ? result : Promise.resolve(result) + }, + "getState", + null, + ) if (extensionState && this.currentState) { // Merge extension state with current state, preserving CLI context - this.currentState = { + const mergedState: ExtensionState = { ...this.currentState, apiConfiguration: extensionState.apiConfiguration || this.currentState.apiConfiguration, - currentApiConfigName: - extensionState.currentApiConfigName || this.currentState.currentApiConfigName, - listApiConfigMeta: extensionState.listApiConfigMeta || this.currentState.listApiConfigMeta, mode: extensionState.mode || this.currentState.mode, chatMessages: extensionState.chatMessages || this.currentState.chatMessages, - routerModels: extensionState.routerModels || this.currentState.routerModels, } + + // Handle optional properties explicitly to satisfy exactOptionalPropertyTypes + if (extensionState.currentApiConfigName !== undefined) { + mergedState.currentApiConfigName = extensionState.currentApiConfigName + } + if (extensionState.listApiConfigMeta !== undefined) { + mergedState.listApiConfigMeta = extensionState.listApiConfigMeta + } + if (extensionState.routerModels !== undefined) { + mergedState.routerModels = extensionState.routerModels + } + + this.currentState = mergedState logs.debug("Synced state with extension on webview launch", "ExtensionHost") } } catch (error) { @@ -860,7 +959,7 @@ export class ExtensionHost extends EventEmitter { const experiments = configState.experiments || this.currentState?.experiments await this.sendWebviewMessage({ type: "updateExperimental", - values: experiments, + values: experiments as Record, }) } } @@ -896,7 +995,7 @@ export class ExtensionHost extends EventEmitter { } // Methods for webview provider registration (called from VSCode API mock) - registerWebviewProvider(viewId: string, provider: any): void { + registerWebviewProvider(viewId: string, provider: WebviewProvider): void { this.webviewProviders.set(viewId, provider) logs.info(`Webview provider registered: ${viewId}`, "ExtensionHost") } diff --git a/cli/src/host/VSCode.ts b/cli/src/host/VSCode.ts index 8b8eb43ca49..76870574bcf 100644 --- a/cli/src/host/VSCode.ts +++ b/cli/src/host/VSCode.ts @@ -14,12 +14,290 @@ export interface IdentityInfo { // Basic VSCode API types and enums export type Thenable = Promise +// TextDocument interface for VSCode API +export interface TextDocument { + uri: Uri + fileName: string + languageId: string + version: number + isDirty: boolean + isClosed: boolean + lineCount: number + getText(range?: Range): string + lineAt(line: number): TextLine + offsetAt(position: Position): number + positionAt(offset: number): Position + save(): Thenable + validateRange(range: Range): Range + validatePosition(position: Position): Position +} + +export interface TextLine { + text: string + range: Range + rangeIncludingLineBreak: Range + firstNonWhitespaceCharacterIndex: number + isEmptyOrWhitespace: boolean +} + +export interface WorkspaceFoldersChangeEvent { + added: WorkspaceFolder[] + removed: WorkspaceFolder[] +} + +export interface TextDocumentChangeEvent { + document: TextDocument + contentChanges: readonly TextDocumentContentChangeEvent[] +} + +export interface TextDocumentContentChangeEvent { + range: Range + rangeOffset: number + rangeLength: number + text: string +} + +export interface ConfigurationChangeEvent { + affectsConfiguration(section: string, scope?: Uri): boolean +} + +export interface ConfigurationInspect { + key: string + defaultValue?: T + globalValue?: T + workspaceValue?: T + workspaceFolderValue?: T +} + +export interface TextDocumentContentProvider { + provideTextDocumentContent(uri: Uri, token: CancellationToken): Thenable + onDidChange?: (listener: (e: Uri) => void) => Disposable +} + +export interface FileSystemWatcher extends Disposable { + onDidChange: (listener: (e: Uri) => void) => Disposable + onDidCreate: (listener: (e: Uri) => void) => Disposable + onDidDelete: (listener: (e: Uri) => void) => Disposable +} + +export interface RelativePattern { + base: string + pattern: string +} + +// TextEditor and related interfaces +export interface TextEditor { + document: TextDocument + selection: Selection + selections: Selection[] + visibleRanges: Range[] + options: TextEditorOptions + viewColumn?: ViewColumn + edit(callback: (editBuilder: TextEditorEdit) => void): Thenable + insertSnippet( + snippet: unknown, + location?: Position | Range | readonly Position[] | readonly Range[], + ): Thenable + setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: readonly Range[]): void + revealRange(range: Range, revealType?: TextEditorRevealType): void + show(column?: ViewColumn): void + hide(): void +} + +export interface TextEditorOptions { + tabSize?: number + insertSpaces?: boolean + cursorStyle?: number + lineNumbers?: number +} + +export interface TextEditorEdit { + replace(location: Position | Range | Selection, value: string): void + insert(location: Position, value: string): void + delete(location: Range | Selection): void + setEndOfLine(endOfLine: EndOfLine): void +} + +export interface TextEditorSelectionChangeEvent { + textEditor: TextEditor + selections: readonly Selection[] + kind?: number +} + +export interface TextDocumentShowOptions { + viewColumn?: ViewColumn + preserveFocus?: boolean + preview?: boolean + selection?: Range +} + +export interface DecorationRenderOptions { + backgroundColor?: string | ThemeColor + border?: string + borderColor?: string | ThemeColor + borderRadius?: string + borderSpacing?: string + borderStyle?: string + borderWidth?: string + color?: string | ThemeColor + cursor?: string + fontStyle?: string + fontWeight?: string + gutterIconPath?: string | Uri + gutterIconSize?: string + isWholeLine?: boolean + letterSpacing?: string + opacity?: string + outline?: string + outlineColor?: string | ThemeColor + outlineStyle?: string + outlineWidth?: string + overviewRulerColor?: string | ThemeColor + overviewRulerLane?: OverviewRulerLane + rangeBehavior?: DecorationRangeBehavior + textDecoration?: string +} + +export interface Terminal { + name: string + processId: Thenable + creationOptions: Readonly + exitStatus: TerminalExitStatus | undefined + state: TerminalState + sendText(text: string, addNewLine?: boolean): void + show(preserveFocus?: boolean): void + hide(): void + dispose(): void +} + +export interface TerminalOptions { + name?: string + shellPath?: string + shellArgs?: string[] | string + cwd?: string | Uri + env?: { [key: string]: string | null | undefined } + iconPath?: Uri | ThemeIcon + hideFromUser?: boolean + message?: string + strictEnv?: boolean +} + +export interface TerminalExitStatus { + code: number | undefined + reason: number +} + +export interface TerminalState { + isInteractedWith: boolean +} + +export interface TerminalDimensionsChangeEvent { + terminal: Terminal + dimensions: TerminalDimensions +} + +export interface TerminalDimensions { + columns: number + rows: number +} + +export interface TerminalDataWriteEvent { + terminal: Terminal + data: string +} + +export interface WebviewViewProvider { + resolveWebviewView( + webviewView: WebviewView, + context: WebviewViewResolveContext, + token: CancellationToken, + ): Thenable | void +} + +export interface WebviewView { + webview: Webview + viewType: string + title?: string + description?: string + badge?: ViewBadge + show(preserveFocus?: boolean): void + onDidChangeVisibility: (listener: () => void) => Disposable + onDidDispose: (listener: () => void) => Disposable + visible: boolean +} + +export interface Webview { + html: string + options: WebviewOptions + cspSource: string + postMessage(message: unknown): Thenable + onDidReceiveMessage: (listener: (message: unknown) => void) => Disposable + asWebviewUri(localResource: Uri): Uri +} + +export interface WebviewOptions { + enableScripts?: boolean + enableForms?: boolean + localResourceRoots?: readonly Uri[] + portMapping?: readonly WebviewPortMapping[] +} + +export interface WebviewPortMapping { + webviewPort: number + extensionHostPort: number +} + +export interface ViewBadge { + tooltip: string + value: number +} + +export interface WebviewViewResolveContext { + state?: unknown +} + +export interface WebviewViewProviderOptions { + retainContextWhenHidden?: boolean +} + +export interface UriHandler { + handleUri(uri: Uri): void +} + +export interface QuickPickOptions { + placeHolder?: string + canPickMany?: boolean + ignoreFocusOut?: boolean + matchOnDescription?: boolean + matchOnDetail?: boolean +} + +export interface InputBoxOptions { + value?: string + valueSelection?: [number, number] + prompt?: string + placeHolder?: string + password?: boolean + ignoreFocusOut?: boolean + validateInput?(value: string): string | undefined | null | Thenable +} + +export interface OpenDialogOptions { + defaultUri?: Uri + openLabel?: string + canSelectFiles?: boolean + canSelectFolders?: boolean + canSelectMany?: boolean + filters?: { [name: string]: string[] } + title?: string +} + // VSCode EventEmitter implementation export interface Disposable { dispose(): void } -type Listener = (e: T) => any +type Listener = (e: T) => void export class EventEmitter { readonly #listeners = new Set>() @@ -27,7 +305,7 @@ export class EventEmitter { /** * The event listeners can subscribe to. */ - event = (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable => { + event = (listener: (e: T) => void, thisArgs?: unknown, disposables?: Disposable[]): Disposable => { const fn = thisArgs ? listener.bind(thisArgs) : listener this.#listeners.add(fn) const disposable = { @@ -327,8 +605,8 @@ export interface DiagnosticCollection extends Disposable { delete(uri: Uri): void clear(): void forEach( - callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, - thisArg?: any, + callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => void, + thisArg?: unknown, ): void get(uri: Uri): Diagnostic[] | undefined has(uri: Uri): boolean @@ -472,13 +750,13 @@ export class ThemeIcon { // Cancellation Token mock export interface CancellationToken { isCancellationRequested: boolean - onCancellationRequested: (listener: (e: any) => any) => Disposable + onCancellationRequested: (listener: (e: unknown) => void) => Disposable } export class CancellationTokenSource { private _token: CancellationToken private _isCancelled = false - private _onCancellationRequestedEmitter = new EventEmitter() + private _onCancellationRequestedEmitter = new EventEmitter() constructor() { this._token = { @@ -494,7 +772,8 @@ export class CancellationTokenSource { cancel(): void { if (!this._isCancelled) { this._isCancelled = true - ;(this._token as any).isCancellationRequested = true + // Type assertion needed to modify readonly property + ;(this._token as { isCancellationRequested: boolean }).isCancellationRequested = true this._onCancellationRequestedEmitter.fire(undefined) } } @@ -508,10 +787,10 @@ export class CancellationTokenSource { // CodeLens mock export class CodeLens { public range: Range - public command?: { command: string; title: string; arguments?: any[] } | undefined + public command?: { command: string; title: string; arguments?: unknown[] } | undefined public isResolved: boolean = false - constructor(range: Range, command?: { command: string; title: string; arguments?: any[] } | undefined) { + constructor(range: Range, command?: { command: string; title: string; arguments?: unknown[] } | undefined) { this.range = range this.command = command } @@ -526,14 +805,14 @@ export class LanguageModelToolCallPart { constructor( public callId: string, public name: string, - public input: any, + public input: unknown, ) {} } export class LanguageModelToolResultPart { constructor( public callId: string, - public content: any[], + public content: unknown[], ) {} } @@ -647,7 +926,7 @@ export class ExtensionContext { public secrets: SecretStorage public extensionUri: Uri public extensionPath: string - public environmentVariableCollection: any + public environmentVariableCollection: Record = {} public storageUri: Uri | undefined public storagePath: string | undefined public globalStorageUri: Uri @@ -682,8 +961,10 @@ export class ExtensionContext { // Initialize state storage this.workspaceState = new MemoryMemento(path.join(workspaceStoragePath, "workspace-state.json")) - this.globalState = new MemoryMemento(path.join(globalStoragePath, "global-state.json")) as any - this.globalState.setKeysForSync = () => {} // No-op for CLI + const globalMemento = new MemoryMemento(path.join(globalStoragePath, "global-state.json")) + this.globalState = Object.assign(globalMemento, { + setKeysForSync: () => {}, // No-op for CLI + }) this.secrets = new MockSecretStorage(globalStoragePath) } @@ -703,12 +984,12 @@ export class ExtensionContext { export interface Memento { get(key: string): T | undefined get(key: string, defaultValue: T): T - update(key: string, value: any): Thenable + update(key: string, value: unknown): Thenable keys(): readonly string[] } class MemoryMemento implements Memento { - private data: Record = {} + private data: Record = {} private filePath: string constructor(filePath: string) { @@ -742,10 +1023,11 @@ class MemoryMemento implements Memento { } get(key: string, defaultValue?: T): T | undefined { - return this.data[key] !== undefined ? this.data[key] : defaultValue + const value = this.data[key] + return value !== undefined && value !== null ? (value as T) : defaultValue } - async update(key: string, value: any): Promise { + async update(key: string, value: unknown): Promise { if (value === undefined) { delete this.data[key] } else { @@ -768,7 +1050,7 @@ export interface SecretStorage { class MockSecretStorage implements SecretStorage { private secrets: Record = {} - private _onDidChange = new EventEmitter() + private _onDidChange = new EventEmitter<{ key: string }>() private filePath: string constructor(storagePath: string) { @@ -949,11 +1231,11 @@ export class WorkspaceAPI { public name: string | undefined public workspaceFile: Uri | undefined public fs: FileSystemAPI - public textDocuments: any[] = [] - private _onDidChangeWorkspaceFolders = new EventEmitter() - private _onDidOpenTextDocument = new EventEmitter() - private _onDidChangeTextDocument = new EventEmitter() - private _onDidCloseTextDocument = new EventEmitter() + public textDocuments: TextDocument[] = [] + private _onDidChangeWorkspaceFolders = new EventEmitter() + private _onDidOpenTextDocument = new EventEmitter() + private _onDidChangeTextDocument = new EventEmitter() + private _onDidCloseTextDocument = new EventEmitter() private workspacePath: string private context: ExtensionContext @@ -971,26 +1253,26 @@ export class WorkspaceAPI { this.fs = new FileSystemAPI() } - onDidChangeWorkspaceFolders(listener: (event: any) => void): Disposable { + onDidChangeWorkspaceFolders(listener: (event: WorkspaceFoldersChangeEvent) => void): Disposable { return this._onDidChangeWorkspaceFolders.event(listener) } - onDidChangeConfiguration(listener: (event: any) => void): Disposable { + onDidChangeConfiguration(listener: (event: ConfigurationChangeEvent) => void): Disposable { // Create a mock configuration change event emitter - const emitter = new EventEmitter() + const emitter = new EventEmitter() return emitter.event(listener) } - onDidChangeTextDocument(listener: (event: any) => void): Disposable { + onDidChangeTextDocument(listener: (event: TextDocumentChangeEvent) => void): Disposable { return this._onDidChangeTextDocument.event(listener) } - onDidOpenTextDocument(listener: (event: any) => void): Disposable { + onDidOpenTextDocument(listener: (event: TextDocument) => void): Disposable { logs.debug("Registering onDidOpenTextDocument listener", "VSCode.Workspace") return this._onDidOpenTextDocument.event(listener) } - onDidCloseTextDocument(listener: (event: any) => void): Disposable { + onDidCloseTextDocument(listener: (event: TextDocument) => void): Disposable { return this._onDidCloseTextDocument.event(listener) } @@ -1003,7 +1285,7 @@ export class WorkspaceAPI { return Promise.resolve([]) } - async openTextDocument(uri: Uri): Promise { + async openTextDocument(uri: Uri): Promise { logs.debug(`openTextDocument called for: ${uri.fsPath}`, "VSCode.Workspace") // Read file content @@ -1132,7 +1414,7 @@ export class WorkspaceAPI { // Update the in-memory document object to reflect the new content // This is critical for CLI mode where DiffViewProvider reads from the document object - const document = this.textDocuments.find((doc: any) => doc.uri.fsPath === filePath) + const document = this.textDocuments.find((doc: TextDocument) => doc.uri.fsPath === filePath) if (document) { const newLines = newContent.split("\n") @@ -1183,20 +1465,21 @@ export class WorkspaceAPI { } createFileSystemWatcher( - _globPattern: any, + _globPattern: string | RelativePattern, _ignoreCreateEvents?: boolean, _ignoreChangeEvents?: boolean, _ignoreDeleteEvents?: boolean, - ): any { + ): FileSystemWatcher { + const emitter = new EventEmitter() return { - onDidChange: () => ({ dispose: () => {} }), - onDidCreate: () => ({ dispose: () => {} }), - onDidDelete: () => ({ dispose: () => {} }), - dispose: () => {}, + onDidChange: (listener: (e: Uri) => void) => emitter.event(listener), + onDidCreate: (listener: (e: Uri) => void) => emitter.event(listener), + onDidDelete: (listener: (e: Uri) => void) => emitter.event(listener), + dispose: () => emitter.dispose(), } } - registerTextDocumentContentProvider(_scheme: string, _provider: any): Disposable { + registerTextDocumentContentProvider(_scheme: string, _provider: TextDocumentContentProvider): Disposable { return { dispose: () => {} } } } @@ -1211,8 +1494,8 @@ export interface WorkspaceConfiguration { get(section: string): T | undefined get(section: string, defaultValue: T): T has(section: string): boolean - inspect(section: string): any - update(section: string, value: any, configurationTarget?: ConfigurationTarget): Thenable + inspect(section: string): ConfigurationInspect | undefined + update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Thenable } export class MockWorkspaceConfiguration implements WorkspaceConfiguration { @@ -1274,7 +1557,7 @@ export class MockWorkspaceConfiguration implements WorkspaceConfiguration { return this.workspaceMemento.get(fullSection) !== undefined || this.globalMemento.get(fullSection) !== undefined } - inspect(section: string): any { + inspect(section: string): ConfigurationInspect | undefined { const fullSection = this.section ? `${this.section}.${section}` : section const workspaceValue = this.workspaceMemento.get(fullSection) const globalValue = this.globalMemento.get(fullSection) @@ -1292,7 +1575,7 @@ export class MockWorkspaceConfiguration implements WorkspaceConfiguration { return undefined } - async update(section: string, value: any, configurationTarget?: ConfigurationTarget): Promise { + async update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Promise { const fullSection = this.section ? `${this.section}.${section}` : section try { @@ -1324,10 +1607,10 @@ export class MockWorkspaceConfiguration implements WorkspaceConfiguration { } // Method to get all configuration data (useful for debugging and generic config loading) - public getAllConfig(): Record { + public getAllConfig(): Record { const globalKeys = this.globalMemento.keys() const workspaceKeys = this.workspaceMemento.keys() - const allConfig: Record = {} + const allConfig: Record = {} // Add global settings first for (const key of globalKeys) { @@ -1431,7 +1714,7 @@ export class StatusBarItem implements Disposable { // Tab and TabGroup interfaces for VSCode API export interface Tab { - input: TabInputText | any + input: TabInputText | unknown label: string isActive: boolean isDirty: boolean @@ -1484,8 +1767,8 @@ export class TabGroupsAPI { // Window API mock export class WindowAPI { public tabGroups: TabGroupsAPI - public visibleTextEditors: any[] = [] - public _onDidChangeVisibleTextEditors = new EventEmitter() + public visibleTextEditors: TextEditor[] = [] + public _onDidChangeVisibleTextEditors = new EventEmitter() private _workspace?: WorkspaceAPI constructor() { @@ -1524,7 +1807,7 @@ export class WindowAPI { return new StatusBarItem(actualAlignment, actualPriority) } - createTextEditorDecorationType(_options: any): TextEditorDecorationType { + createTextEditorDecorationType(_options: DecorationRenderOptions): TextEditorDecorationType { return new TextEditorDecorationType(`decoration-${Date.now()}`) } @@ -1538,7 +1821,7 @@ export class WindowAPI { hideFromUser?: boolean message?: string strictEnv?: boolean - }): any { + }): Terminal { // Return a mock terminal object return { name: options?.name || "Terminal", @@ -1576,34 +1859,34 @@ export class WindowAPI { return Promise.resolve(undefined) } - showQuickPick(items: string[], _options?: any): Thenable { + showQuickPick(items: string[], _options?: QuickPickOptions): Thenable { // Return first item for CLI return Promise.resolve(items[0]) } - showInputBox(_options?: any): Thenable { + showInputBox(_options?: InputBoxOptions): Thenable { // Return empty string for CLI return Promise.resolve("") } - showOpenDialog(_options?: any): Thenable { + showOpenDialog(_options?: OpenDialogOptions): Thenable { // Return empty array for CLI return Promise.resolve([]) } async showTextDocument( - documentOrUri: any | Uri, - columnOrOptions?: ViewColumn | any, + documentOrUri: TextDocument | Uri, + columnOrOptions?: ViewColumn | TextDocumentShowOptions, _preserveFocus?: boolean, - ): Promise { + ): Promise { // Mock implementation for CLI // In a real VSCode environment, this would open the document in an editor const uri = documentOrUri instanceof Uri ? documentOrUri : documentOrUri.uri logs.debug(`showTextDocument called for: ${uri?.toString() || "unknown"}`, "VSCode.Window") // Create a placeholder editor first so it's in visibleTextEditors when onDidOpenTextDocument fires - const placeholderEditor = { - document: { uri }, + const placeholderEditor: TextEditor = { + document: { uri } as TextDocument, selection: new Selection(new Position(0, 0), new Position(0, 0)), selections: [new Selection(new Position(0, 0), new Position(0, 0))], visibleRanges: [new Range(new Position(0, 0), new Position(0, 0))], @@ -1646,25 +1929,46 @@ export class WindowAPI { return placeholderEditor } - registerWebviewViewProvider(viewId: string, provider: any, _options?: any): Disposable { + registerWebviewViewProvider( + viewId: string, + provider: WebviewViewProvider, + _options?: WebviewViewProviderOptions, + ): Disposable { // Store the provider for later use by ExtensionHost - if ((global as any).__extensionHost) { - const extensionHost = (global as any).__extensionHost + if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { + const extensionHost = ( + global as unknown as { + __extensionHost: { + registerWebviewProvider: (viewId: string, provider: WebviewViewProvider) => void + isInInitialSetup: () => boolean + markWebviewReady: () => void + } + } + ).__extensionHost extensionHost.registerWebviewProvider(viewId, provider) // Set up webview mock that captures messages from the extension const mockWebview = { - postMessage: (message: any) => { + postMessage: (message: unknown): Thenable => { // Forward extension messages to ExtensionHost for CLI consumption - if ((global as any).__extensionHost) { - ;(global as any).__extensionHost.emit("extensionWebviewMessage", message) + if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { + ;( + global as unknown as { + __extensionHost: { emit: (event: string, message: unknown) => void } + } + ).__extensionHost.emit("extensionWebviewMessage", message) } + return Promise.resolve(true) }, - onDidReceiveMessage: (listener: (message: any) => void) => { + onDidReceiveMessage: (listener: (message: unknown) => void) => { // This is how the extension listens for messages from the webview // We need to connect this to our message bridge - if ((global as any).__extensionHost) { - ;(global as any).__extensionHost.on("webviewMessage", listener) + if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { + ;( + global as unknown as { + __extensionHost: { on: (event: string, listener: (message: unknown) => void) => void } + } + ).__extensionHost.on("webviewMessage", listener) } return { dispose: () => {} } }, @@ -1681,11 +1985,11 @@ export class WindowAPI { // Provide the mock webview to the provider if (provider.resolveWebviewView) { const mockWebviewView = { - webview: mockWebview, + webview: mockWebview as Webview, viewType: viewId, title: viewId, - description: undefined, - badge: undefined, + description: undefined as string | undefined, + badge: undefined as ViewBadge | undefined, show: () => {}, onDidChangeVisibility: () => ({ dispose: () => {} }), onDidDispose: () => ({ dispose: () => {} }), @@ -1708,7 +2012,7 @@ export class WindowAPI { ) // Await the result to ensure webview is fully initialized before marking ready - await provider.resolveWebviewView(mockWebviewView, context, {}) + await provider.resolveWebviewView(mockWebviewView as WebviewView, {}, {} as CancellationToken) // Mark webview as ready after resolution completes extensionHost.markWebviewReady() @@ -1721,60 +2025,64 @@ export class WindowAPI { } return { dispose: () => { - if ((global as any).__extensionHost) { - ;(global as any).__extensionHost.unregisterWebviewProvider(viewId) + if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { + ;( + global as unknown as { + __extensionHost: { unregisterWebviewProvider: (viewId: string) => void } + } + ).__extensionHost.unregisterWebviewProvider(viewId) } }, } } - registerUriHandler(_handler: any): Disposable { + registerUriHandler(_handler: UriHandler): Disposable { // Store the URI handler for later use return { dispose: () => {}, } } - onDidChangeTextEditorSelection(listener: (event: any) => void): Disposable { - const emitter = new EventEmitter() + onDidChangeTextEditorSelection(listener: (event: TextEditorSelectionChangeEvent) => void): Disposable { + const emitter = new EventEmitter() return emitter.event(listener) } - onDidChangeActiveTextEditor(listener: (event: any) => void): Disposable { - const emitter = new EventEmitter() + onDidChangeActiveTextEditor(listener: (event: TextEditor | undefined) => void): Disposable { + const emitter = new EventEmitter() return emitter.event(listener) } - onDidChangeVisibleTextEditors(listener: (editors: any[]) => void): Disposable { + onDidChangeVisibleTextEditors(listener: (editors: TextEditor[]) => void): Disposable { return this._onDidChangeVisibleTextEditors.event(listener) } // Terminal event handlers - onDidCloseTerminal(_listener: (terminal: any) => void): Disposable { + onDidCloseTerminal(_listener: (terminal: Terminal) => void): Disposable { return { dispose: () => {} } } - onDidOpenTerminal(_listener: (terminal: any) => void): Disposable { + onDidOpenTerminal(_listener: (terminal: Terminal) => void): Disposable { return { dispose: () => {} } } - onDidChangeActiveTerminal(_listener: (terminal: any) => void): Disposable { + onDidChangeActiveTerminal(_listener: (terminal: Terminal | undefined) => void): Disposable { return { dispose: () => {} } } - onDidChangeTerminalDimensions(_listener: (event: any) => void): Disposable { + onDidChangeTerminalDimensions(_listener: (event: TerminalDimensionsChangeEvent) => void): Disposable { return { dispose: () => {} } } - onDidWriteTerminalData(_listener: (event: any) => void): Disposable { + onDidWriteTerminalData(_listener: (event: TerminalDataWriteEvent) => void): Disposable { return { dispose: () => {} } } - get activeTerminal(): any { + get activeTerminal(): Terminal | undefined { return undefined } - get terminals(): any[] { + get terminals(): Terminal[] { return [] } } @@ -1789,17 +2097,17 @@ export interface WorkspaceConfiguration { get(section: string): T | undefined get(section: string, defaultValue: T): T has(section: string): boolean - inspect(_section: string): any - update(section: string, value: any, configurationTarget?: ConfigurationTarget): Thenable + inspect(_section: string): ConfigurationInspect | undefined + update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Thenable } // Commands API mock // Commands API mock export class CommandsAPI { - private commands: Map any> = new Map() + private commands: Map unknown> = new Map() - registerCommand(command: string, callback: (...args: any[]) => any): Disposable { + registerCommand(command: string, callback: (...args: unknown[]) => unknown): Disposable { this.commands.set(command, callback) return { dispose: () => { @@ -1808,12 +2116,12 @@ export class CommandsAPI { } } - executeCommand(command: string, ...rest: any[]): Thenable { + executeCommand(command: string, ...rest: unknown[]): Thenable { const handler = this.commands.get(command) if (handler) { try { const result = handler(...rest) - return Promise.resolve(result) + return Promise.resolve(result as T) } catch (error) { return Promise.reject(error) } @@ -1828,14 +2136,24 @@ export class CommandsAPI { case "vscode.diff": // Simulate opening a diff view for the CLI // The extension's DiffViewProvider expects this to create a diff editor - return this.handleDiffCommand(rest[0], rest[1], rest[2], rest[3]) as Thenable + return this.handleDiffCommand( + rest[0] as Uri, + rest[1] as Uri, + rest[2] as string | undefined, + rest[3], + ) as Thenable default: logs.warn(`Unknown command: ${command}`, "VSCode.Commands") return Promise.resolve(undefined as T) } } - private async handleDiffCommand(originalUri: Uri, modifiedUri: Uri, title?: string, _options?: any): Promise { + private async handleDiffCommand( + originalUri: Uri, + modifiedUri: Uri, + title?: string, + _options?: unknown, + ): Promise { // The DiffViewProvider is waiting for the modified document to appear in visibleTextEditors // We need to simulate this by opening the document and adding it to visible editors @@ -1851,8 +2169,8 @@ export class CommandsAPI { } // Get the workspace API to open the document - const workspace = (global as any).vscode?.workspace - const window = (global as any).vscode?.window + const workspace = (global as unknown as { vscode?: { workspace?: WorkspaceAPI } }).vscode?.workspace + const window = (global as unknown as { vscode?: { window?: WindowAPI } }).vscode?.window if (!workspace || !window) { logs.warn("[DIFF] VSCode APIs not available for diff command", "VSCode.Commands") @@ -1868,7 +2186,7 @@ export class CommandsAPI { // The document should already be open from the showTextDocument call // Find it in the existing textDocuments logs.info(`[DIFF] Looking for already-opened document: ${modifiedUri.fsPath}`, "VSCode.Commands") - let document = workspace.textDocuments.find((doc: any) => doc.uri.fsPath === modifiedUri.fsPath) + let document = workspace.textDocuments.find((doc: TextDocument) => doc.uri.fsPath === modifiedUri.fsPath) if (!document) { // If not found, open it now @@ -1887,9 +2205,9 @@ export class CommandsAPI { visibleRanges: [new Range(new Position(0, 0), new Position(0, 0))], options: {}, viewColumn: ViewColumn.One, - edit: async (callback: (editBuilder: any) => void) => { + edit: async (callback: (editBuilder: TextEditorEdit) => void) => { // Create a mock edit builder - const editBuilder = { + const editBuilder: TextEditorEdit = { replace: (_range: Range, _text: string) => { // In CLI mode, we don't actually edit here // The DiffViewProvider will handle the actual edits @@ -1901,6 +2219,9 @@ export class CommandsAPI { delete: (_range: Range) => { logs.debug("Mock edit builder delete called", "VSCode.Commands") }, + setEndOfLine: (_endOfLine: EndOfLine) => { + logs.debug("Mock edit builder setEndOfLine called", "VSCode.Commands") + }, } callback(editBuilder) return true @@ -1919,7 +2240,7 @@ export class CommandsAPI { // Check if this editor is already in visibleTextEditors (from showTextDocument) const existingEditor = window.visibleTextEditors.find( - (e: any) => e.document.uri.fsPath === modifiedUri.fsPath, + (e: TextEditor) => e.document.uri.fsPath === modifiedUri.fsPath, ) if (existingEditor) { @@ -2013,7 +2334,7 @@ export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: st StatusBarItem, CancellationToken: class CancellationTokenClass implements CancellationToken { isCancellationRequested = false - onCancellationRequested = (_listener: (e: any) => any) => ({ dispose: () => {} }) + onCancellationRequested = (_listener: (e: unknown) => void) => ({ dispose: () => {} }) }, CancellationTokenSource, CodeLens, @@ -2105,8 +2426,8 @@ export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: st diagnostics.clear() }, forEach: ( - callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, - thisArg?: any, + callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => void, + thisArg?: unknown, ) => { diagnostics.forEach((diags, uriString) => { callback.call(thisArg, Uri.parse(uriString), diags, collection) @@ -2160,9 +2481,9 @@ export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: st dispose = () => {} }, // Add relative pattern - RelativePattern: class { + RelativePattern: class implements RelativePattern { constructor( - public base: any, + public base: string, public pattern: string, ) {} }, @@ -2173,8 +2494,8 @@ export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: st Notification: 15, }, // Add URI handler - UriHandler: class { - handleUri = () => {} + UriHandler: class implements UriHandler { + handleUri = (_uri: Uri) => {} }, } } diff --git a/cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts b/cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts index 4a9068228d9..b335d443abb 100644 --- a/cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts +++ b/cli/src/host/__tests__/ExtensionHost.raceCondition.test.ts @@ -2,6 +2,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" import { ExtensionHost } from "../ExtensionHost.js" import type { WebviewMessage } from "../../types/messages.js" +// Type for accessing private members in tests +interface ExtensionHostInternal { + initializeState: () => void + isActivated: boolean + pendingMessages: WebviewMessage[] + webviewProviders: Map Promise }> +} + describe("ExtensionHost Race Condition Fix", () => { let extensionHost: ExtensionHost @@ -13,9 +21,9 @@ describe("ExtensionHost Race Condition Fix", () => { }) // Initialize state and mark as activated for testing - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as ExtensionHostInternal).initializeState.bind(extensionHost) initializeState() - ;(extensionHost as any).isActivated = true + ;(extensionHost as unknown as ExtensionHostInternal).isActivated = true }) afterEach(() => { @@ -36,7 +44,7 @@ describe("ExtensionHost Race Condition Fix", () => { await extensionHost.sendWebviewMessage(message) // Verify message was queued (check internal state) - const pendingMessages = (extensionHost as any).pendingMessages + const pendingMessages = (extensionHost as unknown as ExtensionHostInternal).pendingMessages expect(pendingMessages).toHaveLength(1) expect(pendingMessages[0]).toEqual(message) }) @@ -54,7 +62,7 @@ describe("ExtensionHost Race Condition Fix", () => { } // Verify all messages were queued - const pendingMessages = (extensionHost as any).pendingMessages + const pendingMessages = (extensionHost as unknown as ExtensionHostInternal).pendingMessages expect(pendingMessages).toHaveLength(3) expect(pendingMessages).toEqual(messages) }) @@ -72,13 +80,16 @@ describe("ExtensionHost Race Condition Fix", () => { const mockProvider = { handleCLIMessage: vi.fn(), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Send message - it should not be queued await extensionHost.sendWebviewMessage(message) // Verify message was not queued - const pendingMessages = (extensionHost as any).pendingMessages + const pendingMessages = (extensionHost as unknown as ExtensionHostInternal).pendingMessages expect(pendingMessages).toHaveLength(0) // Verify message was sent directly @@ -99,13 +110,16 @@ describe("ExtensionHost Race Condition Fix", () => { } // Verify messages are queued - expect((extensionHost as any).pendingMessages).toHaveLength(2) + expect((extensionHost as unknown as ExtensionHostInternal).pendingMessages).toHaveLength(2) // Mock the webview provider const mockProvider = { handleCLIMessage: vi.fn(), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Mark webview as ready - this should flush messages extensionHost.markWebviewReady() @@ -114,7 +128,7 @@ describe("ExtensionHost Race Condition Fix", () => { await new Promise((resolve) => setTimeout(resolve, 10)) // Verify pending messages were cleared - expect((extensionHost as any).pendingMessages).toHaveLength(0) + expect((extensionHost as unknown as ExtensionHostInternal).pendingMessages).toHaveLength(0) // Verify all messages were sent expect(mockProvider.handleCLIMessage).toHaveBeenCalledTimes(2) @@ -135,11 +149,14 @@ describe("ExtensionHost Race Condition Fix", () => { // Mock the webview provider to track call order const callOrder: string[] = [] const mockProvider = { - handleCLIMessage: vi.fn((msg: WebviewMessage) => { + handleCLIMessage: vi.fn(async (msg: WebviewMessage) => { callOrder.push(msg.type) }), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Mark webview as ready extensionHost.markWebviewReady() @@ -192,7 +209,7 @@ describe("ExtensionHost Race Condition Fix", () => { await extensionHost.sendWebviewMessage(newTaskMessage) // Verify message is queued, not processed - const pendingMessages = (extensionHost as any).pendingMessages + const pendingMessages = (extensionHost as unknown as ExtensionHostInternal).pendingMessages expect(pendingMessages).toHaveLength(1) expect(pendingMessages[0].type).toBe("newTask") @@ -200,7 +217,10 @@ describe("ExtensionHost Race Condition Fix", () => { const mockProvider = { handleCLIMessage: vi.fn(), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Now mark webview as ready (simulating resolveWebviewView completion) extensionHost.markWebviewReady() @@ -210,7 +230,7 @@ describe("ExtensionHost Race Condition Fix", () => { // Verify the newTask message was sent AFTER webview was ready expect(mockProvider.handleCLIMessage).toHaveBeenCalledWith(newTaskMessage) - expect((extensionHost as any).pendingMessages).toHaveLength(0) + expect((extensionHost as unknown as ExtensionHostInternal).pendingMessages).toHaveLength(0) }) it("should handle configuration injection before task creation", async () => { @@ -229,17 +249,20 @@ describe("ExtensionHost Race Condition Fix", () => { await extensionHost.sendWebviewMessage(taskMessage) // Both should be queued - const pendingMessages = (extensionHost as any).pendingMessages + const pendingMessages = (extensionHost as unknown as ExtensionHostInternal).pendingMessages expect(pendingMessages).toHaveLength(2) // Mock provider const callOrder: string[] = [] const mockProvider = { - handleCLIMessage: vi.fn((msg: WebviewMessage) => { + handleCLIMessage: vi.fn(async (msg: WebviewMessage) => { callOrder.push(msg.type) }), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Mark ready extensionHost.markWebviewReady() @@ -262,7 +285,7 @@ describe("ExtensionHost Race Condition Fix", () => { it("should handle empty pending messages queue", () => { // Mark ready with no pending messages - expect((extensionHost as any).pendingMessages).toHaveLength(0) + expect((extensionHost as unknown as ExtensionHostInternal).pendingMessages).toHaveLength(0) // Should not throw expect(() => extensionHost.markWebviewReady()).not.toThrow() @@ -283,7 +306,10 @@ describe("ExtensionHost Race Condition Fix", () => { } }), } - ;(extensionHost as any).webviewProviders.set("kilo-code.SidebarProvider", mockProvider) + ;(extensionHost as unknown as ExtensionHostInternal).webviewProviders.set( + "kilo-code.SidebarProvider", + mockProvider, + ) // Mark ready and flush extensionHost.markWebviewReady() diff --git a/cli/src/host/__tests__/ExtensionHost.telemetry.test.ts b/cli/src/host/__tests__/ExtensionHost.telemetry.test.ts index 7844ddeef50..5a120155a59 100644 --- a/cli/src/host/__tests__/ExtensionHost.telemetry.test.ts +++ b/cli/src/host/__tests__/ExtensionHost.telemetry.test.ts @@ -14,7 +14,9 @@ describe("ExtensionHost Telemetry Configuration", () => { // Access the private initializeState method through reflection // This tests that the initial state is correctly set to "unset" - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as { initializeState: () => void }).initializeState.bind( + extensionHost, + ) initializeState() const api = extensionHost.getAPI() @@ -31,7 +33,9 @@ describe("ExtensionHost Telemetry Configuration", () => { extensionRootPath: "/test", }) - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as { initializeState: () => void }).initializeState.bind( + extensionHost, + ) initializeState() const api = extensionHost.getAPI() @@ -43,7 +47,7 @@ describe("ExtensionHost Telemetry Configuration", () => { describe("Configuration Injection", () => { let extensionHost: ExtensionHost - let sendWebviewMessageSpy: any + let sendWebviewMessageSpy: ReturnType beforeEach(() => { extensionHost = new ExtensionHost({ @@ -53,11 +57,13 @@ describe("ExtensionHost Telemetry Configuration", () => { }) // Initialize state - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as { initializeState: () => void }).initializeState.bind( + extensionHost, + ) initializeState() // Spy on sendWebviewMessage - sendWebviewMessageSpy = vi.spyOn(extensionHost, "sendWebviewMessage") + sendWebviewMessageSpy = vi.spyOn(extensionHost, "sendWebviewMessage") as ReturnType }) it("should update state with telemetrySetting from config", async () => { @@ -135,7 +141,9 @@ describe("ExtensionHost Telemetry Configuration", () => { }) // Initialize state - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as { initializeState: () => void }).initializeState.bind( + extensionHost, + ) initializeState() }) @@ -194,7 +202,7 @@ describe("ExtensionHost Telemetry Configuration", () => { describe("syncConfigurationMessages", () => { let extensionHost: ExtensionHost - let sendWebviewMessageSpy: any + let sendWebviewMessageSpy: ReturnType beforeEach(() => { extensionHost = new ExtensionHost({ @@ -204,11 +212,13 @@ describe("ExtensionHost Telemetry Configuration", () => { }) // Initialize state - const initializeState = (extensionHost as any).initializeState.bind(extensionHost) + const initializeState = (extensionHost as unknown as { initializeState: () => void }).initializeState.bind( + extensionHost, + ) initializeState() // Spy on sendWebviewMessage - sendWebviewMessageSpy = vi.spyOn(extensionHost, "sendWebviewMessage") + sendWebviewMessageSpy = vi.spyOn(extensionHost, "sendWebviewMessage") as ReturnType }) it("should send telemetrySetting message when telemetry is in config", async () => { diff --git a/cli/src/host/__tests__/console-interception.test.ts b/cli/src/host/__tests__/console-interception.test.ts index 47adb1efddb..0d4438b8f4f 100644 --- a/cli/src/host/__tests__/console-interception.test.ts +++ b/cli/src/host/__tests__/console-interception.test.ts @@ -48,7 +48,7 @@ describe("ExtensionHost Console Interception", () => { it("should clean up global __interceptedConsole on deactivation", async () => { // Verify that the global console interception is cleaned up - expect((global as any).__interceptedConsole).toBeUndefined() + expect("__interceptedConsole" in global).toBe(false) }) }) diff --git a/cli/src/host/__tests__/error-isolation.test.ts b/cli/src/host/__tests__/error-isolation.test.ts index 8742195b6e9..15f9ed07d25 100644 --- a/cli/src/host/__tests__/error-isolation.test.ts +++ b/cli/src/host/__tests__/error-isolation.test.ts @@ -28,7 +28,8 @@ describe("ExtensionHost Error Isolation", () => { describe("Error Classification", () => { it("should identify task abortion errors as expected", () => { extensionHost = new ExtensionHost(mockOptions) - const isExpectedError = (extensionHost as any).isExpectedError + const isExpectedError = (extensionHost as unknown as { isExpectedError: (error: unknown) => boolean }) + .isExpectedError const taskAbortError = new Error("[Task#presentAssistantMessage] task 123.456 aborted") expect(isExpectedError(taskAbortError)).toBe(true) @@ -36,7 +37,8 @@ describe("ExtensionHost Error Isolation", () => { it("should identify other errors as unexpected", () => { extensionHost = new ExtensionHost(mockOptions) - const isExpectedError = (extensionHost as any).isExpectedError + const isExpectedError = (extensionHost as unknown as { isExpectedError: (error: unknown) => boolean }) + .isExpectedError const genericError = new Error("Something went wrong") expect(isExpectedError(genericError)).toBe(false) @@ -44,7 +46,8 @@ describe("ExtensionHost Error Isolation", () => { it("should handle null/undefined errors", () => { extensionHost = new ExtensionHost(mockOptions) - const isExpectedError = (extensionHost as any).isExpectedError + const isExpectedError = (extensionHost as unknown as { isExpectedError: (error: unknown) => boolean }) + .isExpectedError expect(isExpectedError(null)).toBe(false) expect(isExpectedError(undefined)).toBe(false) @@ -54,7 +57,11 @@ describe("ExtensionHost Error Isolation", () => { describe("Safe Execution", () => { it("should execute successful operations normally", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) const result = await safeExecute(() => "success", "test-operation") expect(result).toBe("success") @@ -62,7 +69,11 @@ describe("ExtensionHost Error Isolation", () => { it("should catch and log unexpected errors", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) const errorSpy = vi.fn() extensionHost.on("extension-error", errorSpy) @@ -93,7 +104,11 @@ describe("ExtensionHost Error Isolation", () => { it("should not emit events for expected errors", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) const errorSpy = vi.fn() extensionHost.on("extension-error", errorSpy) @@ -113,9 +128,13 @@ describe("ExtensionHost Error Isolation", () => { it("should track error count", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) - const health = (extensionHost as any).extensionHealth + const health = (extensionHost as unknown as { extensionHealth: { errorCount: number } }).extensionHealth expect(health.errorCount).toBe(0) await safeExecute(() => { @@ -131,9 +150,15 @@ describe("ExtensionHost Error Isolation", () => { it("should update last error information", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) - - const health = (extensionHost as any).extensionHealth + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) + + const health = ( + extensionHost as unknown as { extensionHealth: { lastError: Error | null; lastErrorTime: number } } + ).extensionHealth const testError = new Error("Test error") await safeExecute(() => { @@ -148,9 +173,13 @@ describe("ExtensionHost Error Isolation", () => { describe("Error Event Handling", () => { it("should emit extension-error events with correct structure", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) - const errorEvents: any[] = [] + const errorEvents: Array<{ context: string; error: Error; recoverable: boolean; timestamp: number }> = [] extensionHost.on("extension-error", (event) => errorEvents.push(event)) const testError = new Error("Test error") @@ -169,7 +198,11 @@ describe("ExtensionHost Error Isolation", () => { it("should not emit legacy error events", async () => { extensionHost = new ExtensionHost(mockOptions) - const safeExecute = (extensionHost as any).safeExecute.bind(extensionHost) + const safeExecute = ( + extensionHost as unknown as { + safeExecute: (fn: () => T, context: string, fallback?: T) => Promise + } + ).safeExecute.bind(extensionHost) const errorSpy = vi.fn() extensionHost.on("error", errorSpy) @@ -186,7 +219,16 @@ describe("ExtensionHost Error Isolation", () => { describe("Health Tracking", () => { it("should initialize with healthy state", () => { extensionHost = new ExtensionHost(mockOptions) - const health = (extensionHost as any).extensionHealth + const health = ( + extensionHost as unknown as { + extensionHealth: { + isHealthy: boolean + errorCount: number + lastError: Error | null + lastErrorTime: number + } + } + ).extensionHealth expect(health.isHealthy).toBe(true) expect(health.errorCount).toBe(0) @@ -196,7 +238,17 @@ describe("ExtensionHost Error Isolation", () => { it("should provide health information", () => { extensionHost = new ExtensionHost(mockOptions) - const health = (extensionHost as any).extensionHealth + const health = ( + extensionHost as unknown as { + extensionHealth: { + isHealthy: boolean + errorCount: number + lastError: Error | null + lastErrorTime: number + maxErrorsBeforeWarning: number + } + } + ).extensionHealth expect(health).toMatchObject({ isHealthy: true, diff --git a/cli/src/host/__tests__/webview-async-resolution.test.ts b/cli/src/host/__tests__/webview-async-resolution.test.ts index 049fed6dcf0..619260cab88 100644 --- a/cli/src/host/__tests__/webview-async-resolution.test.ts +++ b/cli/src/host/__tests__/webview-async-resolution.test.ts @@ -73,7 +73,11 @@ describe("Webview Async Resolution", () => { extensionHost.registerWebviewProvider("test-provider", mockProvider) // Simulate the webview registration flow - const vscode = (global as any).vscode + const vscode = ( + global as { + vscode?: { window?: { registerWebviewViewProvider: (viewId: string, provider: unknown) => void } } + } + ).vscode if (vscode && vscode.window) { // This will trigger resolveWebviewView vscode.window.registerWebviewViewProvider("test-provider", mockProvider) @@ -116,7 +120,11 @@ describe("Webview Async Resolution", () => { extensionHost.registerWebviewProvider("test-provider", mockProvider) // Simulate the webview registration flow - const vscode = (global as any).vscode + const vscode = ( + global as { + vscode?: { window?: { registerWebviewViewProvider: (viewId: string, provider: unknown) => void } } + } + ).vscode if (vscode && vscode.window) { vscode.window.registerWebviewViewProvider("test-provider", mockProvider) } @@ -134,12 +142,12 @@ describe("Webview Async Resolution", () => { resolvePromise = resolve }) - const receivedMessages: any[] = [] + const receivedMessages: Array<{ type: string }> = [] const mockProvider = { resolveWebviewView: vi.fn(async () => { await asyncResolution }), - handleCLIMessage: vi.fn((message: any) => { + handleCLIMessage: vi.fn(async (message: { type: string }) => { receivedMessages.push(message) }), } @@ -151,7 +159,11 @@ describe("Webview Async Resolution", () => { extensionHost.registerWebviewProvider("kilo-code.SidebarProvider", mockProvider) // Simulate the webview registration flow - const vscode = (global as any).vscode + const vscode = ( + global as { + vscode?: { window?: { registerWebviewViewProvider: (viewId: string, provider: unknown) => void } } + } + ).vscode if (vscode && vscode.window) { vscode.window.registerWebviewViewProvider("kilo-code.SidebarProvider", mockProvider) } diff --git a/cli/src/host/__tests__/workspace-applyEdit-sync.test.ts b/cli/src/host/__tests__/workspace-applyEdit-sync.test.ts index 7494b046ad9..101a357aa4e 100644 --- a/cli/src/host/__tests__/workspace-applyEdit-sync.test.ts +++ b/cli/src/host/__tests__/workspace-applyEdit-sync.test.ts @@ -6,7 +6,7 @@ import * as os from "os" describe("WorkspaceAPI.applyEdit Document Synchronization", () => { let tempDir: string - let vscodeAPI: any + let vscodeAPI: ReturnType let testFilePath: string beforeEach(() => { diff --git a/cli/src/parallel/parallel.ts b/cli/src/parallel/parallel.ts index d04175a213b..b5ff63beb50 100644 --- a/cli/src/parallel/parallel.ts +++ b/cli/src/parallel/parallel.ts @@ -124,7 +124,7 @@ export async function finishParallelMode(cli: CLI, worktreePath: string, worktre await service.sendWebviewMessage({ type: "askResponse", - askResponse: agentCommitInstruction, + askResponse: "messageResponse", text: agentCommitInstruction, }) diff --git a/cli/src/services/__tests__/ExtensionService.test.ts b/cli/src/services/__tests__/ExtensionService.test.ts index 5c056668fc2..7eef9eb4ba8 100644 --- a/cli/src/services/__tests__/ExtensionService.test.ts +++ b/cli/src/services/__tests__/ExtensionService.test.ts @@ -13,9 +13,9 @@ vi.mock("../../utils/extension-paths.js", () => ({ describe("ExtensionService", () => { let service: ExtensionService - let mockExtensionModule: any - let originalRequire: any - let mockVSCodeAPI: any + let mockExtensionModule: unknown + let originalRequire: unknown + let mockVSCodeAPI: unknown beforeAll(() => { // Create a mock VSCode API @@ -95,14 +95,17 @@ describe("ExtensionService", () => { // Create a mock extension module mockExtensionModule = { - activate: vi.fn(async (context) => { + activate: vi.fn(async (_context) => { // Register a mock webview provider immediately to prevent hanging // This simulates the extension registering its provider during activation - if ((global as any).__extensionHost) { + const globalWithHost = global as { + __extensionHost?: { registerWebviewProvider: (id: string, provider: unknown) => void } + } + if (globalWithHost.__extensionHost) { const mockProvider = { handleCLIMessage: vi.fn(async () => {}), } - ;(global as any).__extensionHost.registerWebviewProvider("kilo-code.SidebarProvider", mockProvider) + globalWithHost.__extensionHost.registerWebviewProvider("kilo-code.SidebarProvider", mockProvider) } // Return a mock API @@ -144,18 +147,18 @@ describe("ExtensionService", () => { const Module = require("module") originalRequire = Module.prototype.require - Module.prototype.require = function (this: any, id: string) { + Module.prototype.require = function (this: unknown, id: string) { if (id === "/mock/extension/dist/extension.js") { return mockExtensionModule } if (id === "vscode" || id === "vscode-mock") { return mockVSCodeAPI } - return originalRequire.call(this, id) + return (originalRequire as (id: string) => unknown).call(this, id) } // Set global vscode - ;(global as any).vscode = mockVSCodeAPI + ;(global as unknown as { vscode: unknown }).vscode = mockVSCodeAPI }) afterAll(() => { @@ -166,7 +169,7 @@ describe("ExtensionService", () => { Module.prototype.require = originalRequire } // Clean up global vscode - delete (global as any).vscode + delete (global as unknown as { vscode?: unknown }).vscode }) afterEach(async () => { diff --git a/cli/src/services/__tests__/approvalDecision.test.ts b/cli/src/services/__tests__/approvalDecision.test.ts index 1f6fa7237ed..a52b55e2938 100644 --- a/cli/src/services/__tests__/approvalDecision.test.ts +++ b/cli/src/services/__tests__/approvalDecision.test.ts @@ -64,7 +64,7 @@ describe("approvalDecision", () => { describe("getApprovalDecision", () => { describe("non-ask messages", () => { it("should return manual for non-ask messages", () => { - const message = { ...createMessage("tool"), type: "say" } as any + const message = { ...createMessage("tool"), type: "say" } as ExtensionChatMessage const config = createBaseConfig() const decision = getApprovalDecision(message, config, false) expect(decision.action).toBe("manual") diff --git a/cli/src/services/approvalDecision.ts b/cli/src/services/approvalDecision.ts index 94681ab778a..ceab5ce17f7 100644 --- a/cli/src/services/approvalDecision.ts +++ b/cli/src/services/approvalDecision.ts @@ -342,7 +342,6 @@ export function getApprovalDecision( // Handle MCP server requests (extension uses this as ask type instead of "tool") case "use_mcp_server": - case "access_mcp_resource": if (config.mcp?.enabled) { return { action: "auto-approve" } } diff --git a/cli/src/services/autocomplete.ts b/cli/src/services/autocomplete.ts index 73ee23e724f..46bf1c13676 100644 --- a/cli/src/services/autocomplete.ts +++ b/cli/src/services/autocomplete.ts @@ -13,6 +13,7 @@ import type { ArgumentDefinition, InputState, ArgumentProvider, + ArgumentProviderCommandContext, } from "../commands/core/types.js" // ============================================================================ @@ -443,7 +444,7 @@ function createProviderContext( currentArgs: string[], argumentIndex: number, partialInput: string, - commandContext?: any, + commandContext?: ArgumentProviderCommandContext, ): ArgumentProviderContext { const argumentDef = command.arguments?.[argumentIndex] @@ -496,7 +497,7 @@ function getProvider( command: Command, currentArgs: string[], argumentIndex: number, - commandContext?: any, + commandContext?: ArgumentProviderCommandContext, ): ArgumentProvider | null { // Check conditional providers if (definition.conditionalProviders) { @@ -590,7 +591,10 @@ function getCacheKey(definition: ArgumentDefinition, command: Command, index: nu /** * Get argument suggestions for current input */ -export async function getArgumentSuggestions(input: string, commandContext?: any): Promise { +export async function getArgumentSuggestions( + input: string, + commandContext?: ArgumentProviderCommandContext, +): Promise { const state = detectInputState(input) if (state.type !== "argument" || !state.currentArgument) { @@ -775,7 +779,7 @@ export async function getFileMentionSuggestions( export async function getAllSuggestions( input: string, cursorPosition: number, - commandContext?: any, + commandContext?: ArgumentProviderCommandContext, cwd?: string, ): Promise< | { type: "command"; suggestions: CommandSuggestion[] } diff --git a/cli/src/services/extension.ts b/cli/src/services/extension.ts index 056b64968c0..b14c41eea54 100644 --- a/cli/src/services/extension.ts +++ b/cli/src/services/extension.ts @@ -123,19 +123,22 @@ export class ExtensionService extends EventEmitter { }) // Handle new extension-error events (non-fatal errors from extension) - this.extensionHost.on("extension-error", (errorEvent: any) => { - const { context, error, recoverable } = errorEvent - - if (recoverable) { - logs.warn(`Recoverable extension error in ${context}`, "ExtensionService", { error }) - // Emit warning event instead of error to prevent crashes - this.emit("warning", { context, error }) - } else { - logs.error(`Critical extension error in ${context}`, "ExtensionService", { error }) - // Still emit error but don't crash - this.emit("error", error) - } - }) + this.extensionHost.on( + "extension-error", + (errorEvent: { context: string; error: Error; recoverable: boolean }) => { + const { context, error, recoverable } = errorEvent + + if (recoverable) { + logs.warn(`Recoverable extension error in ${context}`, "ExtensionService", { error }) + // Emit warning event instead of error to prevent crashes + this.emit("warning", { context, error }) + } else { + logs.error(`Critical extension error in ${context}`, "ExtensionService", { error }) + // Still emit error but don't crash + this.emit("error", error) + } + }, + ) // Keep backward compatibility for "error" events but don't propagate to prevent crashes this.extensionHost.on("error", (error: Error) => { @@ -175,12 +178,14 @@ export class ExtensionService extends EventEmitter { /** * Handle TUI messages and return response */ - private async handleTUIMessage(data: any): Promise { + private async handleTUIMessage(data: unknown): Promise { try { - if (data.type === "webviewMessage") { - const message = data.payload - await this.extensionHost.sendWebviewMessage(message) - return { success: true } + if (typeof data === "object" && data !== null && "type" in data) { + const typedData = data as { type: string; payload?: WebviewMessage } + if (typedData.type === "webviewMessage" && typedData.payload) { + await this.extensionHost.sendWebviewMessage(typedData.payload) + return { success: true } + } } return { success: true } @@ -303,7 +308,13 @@ export class ExtensionService extends EventEmitter { if (!this.extensionHost) { return null } - return (this.extensionHost as any).extensionHealth || null + return ( + ( + this.extensionHost as unknown as { + extensionHealth?: { isHealthy: boolean; errorCount: number; lastError: Error | null } + } + ).extensionHealth || null + ) } /** @@ -353,11 +364,11 @@ export class ExtensionService extends EventEmitter { * Type-safe event emitter methods */ override on(event: K, listener: ExtensionServiceEvents[K]): this { - return super.on(event, listener as any) + return super.on(event, listener as (...args: unknown[]) => void) } override once(event: K, listener: ExtensionServiceEvents[K]): this { - return super.once(event, listener as any) + return super.once(event, listener as (...args: unknown[]) => void) } override emit( @@ -368,7 +379,7 @@ export class ExtensionService extends EventEmitter { } override off(event: K, listener: ExtensionServiceEvents[K]): this { - return super.off(event, listener as any) + return super.off(event, listener as (...args: unknown[]) => void) } } diff --git a/cli/src/services/logs.ts b/cli/src/services/logs.ts index 2d7fc14dad5..992ce75e363 100644 --- a/cli/src/services/logs.ts +++ b/cli/src/services/logs.ts @@ -11,7 +11,7 @@ export interface LogEntry { level: LogLevel message: string source?: string - context?: Record + context?: Record } export interface LogFilter { @@ -70,7 +70,7 @@ export class LogsService { /** * Serialize an error object to a plain object with all relevant properties */ - private serializeError(error: any): any { + private serializeError(error: unknown): unknown { if (error instanceof Error) { return { message: error.message, @@ -81,10 +81,10 @@ export class LogsService { .filter((key) => key !== "message" && key !== "name" && key !== "stack") .reduce( (acc, key) => { - acc[key] = (error as any)[key] + acc[key] = (error as unknown as Record)[key] return acc }, - {} as Record, + {} as Record, ), } } @@ -94,18 +94,18 @@ export class LogsService { /** * Serialize context object, handling Error objects specially */ - private serializeContext(context?: Record): Record | undefined { + private serializeContext(context?: Record): Record | undefined { if (!context) { return undefined } - const serialized: Record = {} + const serialized: Record = {} for (const [key, value] of Object.entries(context)) { if (value instanceof Error) { serialized[key] = this.serializeError(value) } else if (typeof value === "object" && value !== null) { // Recursively handle nested objects that might contain errors - serialized[key] = this.serializeContext(value as Record) || value + serialized[key] = this.serializeContext(value as Record) || value } else { serialized[key] = value } @@ -116,7 +116,7 @@ export class LogsService { /** * Add a log entry with the specified level */ - private addLog(level: LogLevel, message: string, source?: string, context?: Record): void { + private addLog(level: LogLevel, message: string, source?: string, context?: Record): void { // Serialize context to handle Error objects properly const serializedContext = this.serializeContext(context) @@ -155,7 +155,7 @@ export class LogsService { */ private outputToConsole(entry: LogEntry): void { // GUARD: Prevent recursive logging by checking if we're already in a logging call - if ((this as any)._isLogging) { + if ((this as { _isLogging?: boolean })._isLogging) { return } @@ -166,7 +166,7 @@ export class LogsService { } // Set flag to prevent recursion - ;(this as any)._isLogging = true + ;(this as { _isLogging?: boolean })._isLogging = true try { const ts = new Date(entry.ts).toISOString() @@ -197,7 +197,7 @@ export class LogsService { } } finally { // Always clear the flag - ;(this as any)._isLogging = false + ;(this as { _isLogging?: boolean })._isLogging = false } } @@ -268,28 +268,28 @@ export class LogsService { /** * Log an info message */ - public info(message: string, source?: string, context?: Record): void { + public info(message: string, source?: string, context?: Record): void { this.addLog("info", message, source, context) } /** * Log a debug message */ - public debug(message: string, source?: string, context?: Record): void { + public debug(message: string, source?: string, context?: Record): void { this.addLog("debug", message, source, context) } /** * Log an error message */ - public error(message: string, source?: string, context?: Record): void { + public error(message: string, source?: string, context?: Record): void { this.addLog("error", message, source, context) } /** * Log a warning message */ - public warn(message: string, source?: string, context?: Record): void { + public warn(message: string, source?: string, context?: Record): void { this.addLog("warn", message, source, context) } diff --git a/cli/src/services/telemetry/TelemetryClient.ts b/cli/src/services/telemetry/TelemetryClient.ts index e18961b26f7..2804c04baa6 100644 --- a/cli/src/services/telemetry/TelemetryClient.ts +++ b/cli/src/services/telemetry/TelemetryClient.ts @@ -14,7 +14,7 @@ import { logs } from "../logs.js" */ interface QueuedEvent { event: string - properties: Record + properties: Record timestamp: number retryCount: number } @@ -126,7 +126,7 @@ export class TelemetryClient { /** * Capture a telemetry event */ - public capture(event: TelemetryEvent, properties: Record = {}): void { + public capture(event: TelemetryEvent, properties: Record = {}): void { if (!this.config.enabled || !this.client || !this.identity || this.isShuttingDown) { return } @@ -164,7 +164,7 @@ export class TelemetryClient { /** * Capture an exception */ - public captureException(error: Error, properties: Record = {}): void { + public captureException(error: Error, properties: Record = {}): void { this.capture(TelemetryEvent.EXCEPTION_CAUGHT, { errorType: error.name, errorMessage: error.message, @@ -191,7 +191,12 @@ export class TelemetryClient { /** * Track API request */ - public trackApiRequest(provider: string, model: string, responseTime: number, tokens?: any): void { + public trackApiRequest( + provider: string, + model: string, + responseTime: number, + tokens?: Record, + ): void { this.performanceMetrics.totalApiRequests++ this.performanceMetrics.apiResponseTimes.push(responseTime) @@ -229,7 +234,7 @@ export class TelemetryClient { /** * Get performance metrics */ - public getPerformanceMetrics(): any { + public getPerformanceMetrics(): Record { const memory = process.memoryUsage() return { diff --git a/cli/src/services/telemetry/TelemetryService.ts b/cli/src/services/telemetry/TelemetryService.ts index 35ce0af499a..f996b1b9084 100644 --- a/cli/src/services/telemetry/TelemetryService.ts +++ b/cli/src/services/telemetry/TelemetryService.ts @@ -77,7 +77,7 @@ export class TelemetryService { // Update Kilocode user ID if token is available const provider = config.providers.find((p) => p.id === config.provider) - if (provider && provider.kilocodeToken) { + if (provider && provider.kilocodeToken && typeof provider.kilocodeToken === "string") { await identityManager.updateKilocodeUserId(provider.kilocodeToken) } @@ -263,7 +263,7 @@ export class TelemetryService { }) } - public trackTaskCompleted(taskId: string, duration: number, stats: any): void { + public trackTaskCompleted(taskId: string, duration: number, stats: Record): void { if (!this.client) return this.client.capture(TelemetryEvent.TASK_COMPLETED, { @@ -367,7 +367,12 @@ export class TelemetryService { // Tool Tracking // ============================================================================ - public trackToolExecuted(toolName: string, executionTime: number, success: boolean, metadata?: any): void { + public trackToolExecuted( + toolName: string, + executionTime: number, + success: boolean, + metadata?: Record, + ): void { if (!this.client) return this.client.trackToolExecution(toolName, executionTime, success) @@ -486,7 +491,12 @@ export class TelemetryService { // Performance Tracking // ============================================================================ - public trackApiRequest(provider: string, model: string, responseTime: number, tokens?: any): void { + public trackApiRequest( + provider: string, + model: string, + responseTime: number, + tokens?: Record, + ): void { if (!this.client) return this.client.trackApiRequest(provider, model, responseTime, tokens) @@ -551,7 +561,7 @@ export class TelemetryService { }) } - public trackCIModeCompleted(stats: any): void { + public trackCIModeCompleted(stats: Record): void { if (!this.client) return this.client.capture(TelemetryEvent.CI_MODE_COMPLETED, { diff --git a/cli/src/services/telemetry/__tests__/telemetry.test.ts b/cli/src/services/telemetry/__tests__/telemetry.test.ts index ae5e62c24df..9f1e36b3503 100644 --- a/cli/src/services/telemetry/__tests__/telemetry.test.ts +++ b/cli/src/services/telemetry/__tests__/telemetry.test.ts @@ -184,7 +184,7 @@ describe("TelemetryService", () => { beforeEach(() => { // Reset singleton - ;(TelemetryService as any).instance = null + ;(TelemetryService as unknown as { instance: TelemetryService | null }).instance = null service = TelemetryService.getInstance() }) @@ -329,7 +329,7 @@ describe("IdentityManager", () => { beforeEach(() => { // Reset singleton - ;(IdentityManager as any).instance = null + ;(IdentityManager as unknown as { instance: IdentityManager | null }).instance = null identityManager = IdentityManager.getInstance() }) diff --git a/cli/src/services/telemetry/events.ts b/cli/src/services/telemetry/events.ts index 6254c3fdd40..eb62e49f0b0 100644 --- a/cli/src/services/telemetry/events.ts +++ b/cli/src/services/telemetry/events.ts @@ -333,13 +333,13 @@ export interface FeatureUsageProperties extends BaseProperties { /** * Type guard to check if properties are valid */ -export function isValidEventProperties(properties: any): properties is BaseProperties { +export function isValidEventProperties(properties: unknown): properties is BaseProperties { return ( typeof properties === "object" && properties !== null && - typeof properties.cliVersion === "string" && - typeof properties.sessionId === "string" && - typeof properties.mode === "string" && - typeof properties.ciMode === "boolean" + typeof (properties as Record).cliVersion === "string" && + typeof (properties as Record).sessionId === "string" && + typeof (properties as Record).mode === "string" && + typeof (properties as Record).ciMode === "boolean" ) } diff --git a/cli/src/services/telemetry/identity.ts b/cli/src/services/telemetry/identity.ts index 9b79cbb5a9f..148f607e928 100644 --- a/cli/src/services/telemetry/identity.ts +++ b/cli/src/services/telemetry/identity.ts @@ -250,13 +250,13 @@ export class IdentityManager { /** * Validate stored identity data */ - private isValidStoredIdentity(data: any): data is StoredIdentity { + private isValidStoredIdentity(data: unknown): data is StoredIdentity { return ( typeof data === "object" && data !== null && - typeof data.cliUserId === "string" && - typeof data.createdAt === "number" && - typeof data.lastUsed === "number" + typeof (data as Record).cliUserId === "string" && + typeof (data as Record).createdAt === "number" && + typeof (data as Record).lastUsed === "number" ) } diff --git a/cli/src/state/atoms/__tests__/approval.test.ts b/cli/src/state/atoms/__tests__/approval.test.ts index dec6ff8d259..461f462d452 100644 --- a/cli/src/state/atoms/__tests__/approval.test.ts +++ b/cli/src/state/atoms/__tests__/approval.test.ts @@ -3,7 +3,6 @@ */ import { describe, it, expect } from "vitest" -import { atom } from "jotai" import { createStore } from "jotai" import { approvalOptionsAtom, pendingApprovalAtom } from "../approval.js" import type { ExtensionChatMessage } from "../../../types/messages.js" diff --git a/cli/src/state/atoms/__tests__/keyboard.test.ts b/cli/src/state/atoms/__tests__/keyboard.test.ts index f5dabadfc4e..c32ce7fc122 100644 --- a/cli/src/state/atoms/__tests__/keyboard.test.ts +++ b/cli/src/state/atoms/__tests__/keyboard.test.ts @@ -11,10 +11,11 @@ import { import { textBufferStringAtom, textBufferStateAtom } from "../textBuffer.js" import { keyboardHandlerAtom, submissionCallbackAtom, submitInputAtom } from "../keyboard.js" import { pendingApprovalAtom } from "../approval.js" -import { historyDataAtom, historyModeAtom, historyIndexAtom } from "../history.js" +import { historyDataAtom, historyModeAtom, historyIndexAtom as _historyIndexAtom } from "../history.js" import type { Key } from "../../../types/keyboard.js" import type { CommandSuggestion, ArgumentSuggestion, FileMentionSuggestion } from "../../../services/autocomplete.js" import type { Command } from "../../../commands/core/types.js" +import type { ExtensionChatMessage } from "../../../types/messages.js" describe("keypress atoms", () => { let store: ReturnType @@ -266,7 +267,7 @@ describe("keypress atoms", () => { it("should handle non-function callback gracefully", () => { // Set callback to a non-function value - store.set(submissionCallbackAtom, { callback: "not a function" as any }) + store.set(submissionCallbackAtom, { callback: "not a function" as unknown as (() => void) | null }) // Type 'hello' const chars = ["h", "e", "l", "l", "o"] @@ -300,7 +301,7 @@ describe("keypress atoms", () => { // Submit a Buffer instead of string const buffer = Buffer.from("/help") - store.set(submitInputAtom, buffer as any) + store.set(submitInputAtom, buffer as unknown as string) // Should convert Buffer to string and call callback expect(mockCallback).toHaveBeenCalledWith("/help") @@ -650,12 +651,12 @@ describe("keypress atoms", () => { it("should handle empty approvalOptions array without NaN", () => { // Set up approval mode with a message that produces empty options // (non-ask message type will result in empty approvalOptions) - const mockMessage: any = { + const mockMessage = { ts: Date.now(), type: "say", // Not "ask", so approvalOptions will be empty say: "test", text: "test message", - } + } as ExtensionChatMessage store.set(pendingApprovalAtom, mockMessage) store.set(selectedIndexAtom, 0) @@ -678,12 +679,12 @@ describe("keypress atoms", () => { it("should handle empty approvalOptions array on up arrow without NaN", () => { // Set up approval mode with a message that produces empty options - const mockMessage: any = { + const mockMessage = { ts: Date.now(), type: "say", // Not "ask", so approvalOptions will be empty say: "test", text: "test message", - } + } as ExtensionChatMessage store.set(pendingApprovalAtom, mockMessage) store.set(selectedIndexAtom, 0) diff --git a/cli/src/state/atoms/actions.ts b/cli/src/state/atoms/actions.ts index f891441ac6e..eeb9342a409 100644 --- a/cli/src/state/atoms/actions.ts +++ b/cli/src/state/atoms/actions.ts @@ -4,7 +4,7 @@ */ import { atom } from "jotai" -import type { WebviewMessage } from "../../types/messages.js" +import type { WebviewMessage, ProviderSettings, ClineAskResponse } from "../../types/messages.js" import { extensionServiceAtom, isServiceReadyAtom, setServiceErrorAtom } from "./service.js" import { resetMessageCutoffAtom } from "./ui.js" import { logs } from "../../services/logs.js" @@ -68,16 +68,22 @@ export const sendTaskAtom = atom(null, async (get, set, params: { text: string; */ export const sendAskResponseAtom = atom( null, - async (get, set, params: { response?: string; action?: string; text?: string; images?: string[] }) => { + async ( + get, + set, + params: { + response?: ClineAskResponse + action?: string + text?: string + images?: string[] + }, + ) => { const message: WebviewMessage = { type: "askResponse", } if (params.response) { - message.askResponse = params.response - } - if (params.action) { - message.action = params.action + message.askResponse = params.response || "messageResponse" } if (params.text) { message.text = params.text @@ -169,9 +175,14 @@ export const switchModeAtom = atom(null, async (get, set, mode: string) => { */ export const respondToToolAtom = atom( null, - async (get, set, params: { response: "yesButtonTapped" | "noButtonTapped"; text?: string; images?: string[] }) => { + async ( + get, + set, + params: { response: "yesButtonClicked" | "noButtonClicked"; text?: string; images?: string[] }, + ) => { const message: WebviewMessage = { - type: params.response, + type: "askResponse", + askResponse: params.response, ...(params.text && { text: params.text }), ...(params.images && { images: params.images }), } @@ -183,9 +194,10 @@ export const respondToToolAtom = atom( /** * Action atom to send API configuration */ -export const sendApiConfigurationAtom = atom(null, async (get, set, apiConfiguration: any) => { +export const sendApiConfigurationAtom = atom(null, async (_get, set, apiConfiguration: ProviderSettings) => { const message: WebviewMessage = { - type: "apiConfiguration", + type: "upsertApiConfiguration", + text: "default", apiConfiguration, } @@ -209,7 +221,7 @@ export const sendCustomInstructionsAtom = atom(null, async (get, set, instructio */ export const sendAlwaysAllowAtom = atom(null, async (get, set, alwaysAllow: boolean) => { const message: WebviewMessage = { - type: "alwaysAllow", + type: "alwaysAllowMcp", bool: alwaysAllow, } @@ -217,7 +229,7 @@ export const sendAlwaysAllowAtom = atom(null, async (get, set, alwaysAllow: bool }) /** - * Action atom to open a file in the editor + * Action atom to open a file in the editorMcp */ export const openFileAtom = atom(null, async (get, set, filePath: string) => { const message: WebviewMessage = { @@ -233,7 +245,7 @@ export const openFileAtom = atom(null, async (get, set, filePath: string) => { */ export const openSettingsAtom = atom(null, async (get, set) => { const message: WebviewMessage = { - type: "openSettings", + type: "openExtensionSettings", } await set(sendWebviewMessageAtom, message) @@ -267,7 +279,8 @@ export const refreshStateAtom = atom(null, async (get) => { */ export const sendPrimaryButtonClickAtom = atom(null, async (get, set) => { const message: WebviewMessage = { - type: "primaryButtonClick", + type: "askResponse", + askResponse: "yesButtonClicked", } await set(sendWebviewMessageAtom, message) @@ -278,7 +291,8 @@ export const sendPrimaryButtonClickAtom = atom(null, async (get, set) => { */ export const sendSecondaryButtonClickAtom = atom(null, async (get, set) => { const message: WebviewMessage = { - type: "secondaryButtonClick", + type: "askResponse", + askResponse: "noButtonClicked", } await set(sendWebviewMessageAtom, message) diff --git a/cli/src/state/atoms/approval.ts b/cli/src/state/atoms/approval.ts index eb58ecd54d7..aeb3111dda4 100644 --- a/cli/src/state/atoms/approval.ts +++ b/cli/src/state/atoms/approval.ts @@ -365,7 +365,7 @@ export const executeSelectedCallbackAtom = atom<(() => Promise) | null>(nu * Action atom to approve the pending request * Calls the callback set by the hook */ -export const approveAtom = atom(null, async (get, set) => { +export const approveAtom = atom(null, async (get, _set) => { const callback = get(approveCallbackAtom) if (callback) { await callback() @@ -376,7 +376,7 @@ export const approveAtom = atom(null, async (get, set) => { * Action atom to reject the pending request * Calls the callback set by the hook */ -export const rejectAtom = atom(null, async (get, set) => { +export const rejectAtom = atom(null, async (get, _set) => { const callback = get(rejectCallbackAtom) if (callback) { await callback() @@ -387,7 +387,7 @@ export const rejectAtom = atom(null, async (get, set) => { * Action atom to execute the currently selected option * Calls the callback set by the hook */ -export const executeSelectedAtom = atom(null, async (get, set) => { +export const executeSelectedAtom = atom(null, async (get, _set) => { const callback = get(executeSelectedCallbackAtom) if (callback) { await callback() diff --git a/cli/src/state/atoms/config.ts b/cli/src/state/atoms/config.ts index 7665f6c13b8..811f6764292 100644 --- a/cli/src/state/atoms/config.ts +++ b/cli/src/state/atoms/config.ts @@ -1,5 +1,5 @@ import { atom } from "jotai" -import type { CLIConfig, ProviderConfig } from "../../config/types.js" +import type { AutoApprovalConfig, CLIConfig, ProviderConfig } from "../../config/types.js" import { DEFAULT_CONFIG } from "../../config/defaults.js" import { loadConfig, saveConfig } from "../../config/persistence.js" import { mapConfigToExtensionState } from "../../config/mapper.js" @@ -129,7 +129,7 @@ export const selectProviderAtom = atom(null, async (get, set, providerId: string getTelemetryService().trackProviderChanged( previousProvider, providerId, - provider.apiModelId || provider.kilocodeModel, + (provider.apiModelId as string | undefined) || (provider.kilocodeModel as string | undefined), ) }) @@ -471,7 +471,7 @@ export const updateAutoApprovalAtom = atom( */ export const updateAutoApprovalSettingAtom = atom( null, - async (get, set, category: keyof import("../../config/types.js").AutoApprovalConfig, updates: any) => { + async (get, set, category: keyof AutoApprovalConfig, updates: Record) => { const config = get(configAtom) const updatedConfig = { @@ -479,7 +479,7 @@ export const updateAutoApprovalSettingAtom = atom( autoApproval: { ...config.autoApproval, [category]: { - ...(config.autoApproval?.[category] as any), + ...(config.autoApproval?.[category] as Record), ...updates, }, }, diff --git a/cli/src/state/atoms/effects.ts b/cli/src/state/atoms/effects.ts index 249d4c82966..668b643197e 100644 --- a/cli/src/state/atoms/effects.ts +++ b/cli/src/state/atoms/effects.ts @@ -4,7 +4,8 @@ */ import { atom } from "jotai" -import type { ExtensionMessage } from "../../types/messages.js" +import type { ExtensionMessage, ExtensionChatMessage, RouterModels } from "../../types/messages.js" +import type { HistoryItem } from "@roo-code/types" import { extensionServiceAtom, setServiceReadyAtom, setServiceErrorAtom, setIsInitializingAtom } from "./service.js" import { updateExtensionStateAtom, updateChatMessageByTsAtom, updateRouterModelsAtom } from "./extension.js" import { ciCompletionDetectedAtom } from "./ci.js" @@ -15,6 +16,8 @@ import { setBalanceLoadingAtom, setProfileErrorAtom, setBalanceErrorAtom, + type ProfileData, + type BalanceData, } from "./profile.js" import { taskHistoryDataAtom, @@ -39,7 +42,7 @@ const isProcessingBufferAtom = atom(false) * Effect atom to initialize the ExtensionService * This sets up event listeners and activates the service */ -export const initializeServiceEffectAtom = atom(null, async (get, set, store?: any) => { +export const initializeServiceEffectAtom = atom(null, async (get, set, store?: { set: typeof set }) => { const service = get(extensionServiceAtom) if (!service) { @@ -49,7 +52,7 @@ export const initializeServiceEffectAtom = atom(null, async (get, set, store?: a } // Get the store reference - if not passed, we can't update atoms from event listeners - const atomStore = store || (get as any).store + const atomStore = store || (get as { store?: { set: typeof set } }).store if (!atomStore) { logs.error("No store available for event listeners", "effects") } @@ -137,42 +140,58 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension // Skip processing here to avoid duplication break - case "messageUpdated": - if (message.chatMessage) { - set(updateChatMessageByTsAtom, message.chatMessage) + case "messageUpdated": { + const chatMessage = message.chatMessage as ExtensionChatMessage | undefined + if (chatMessage) { + set(updateChatMessageByTsAtom, chatMessage) } break + } - case "routerModels": - if (message.routerModels) { - set(updateRouterModelsAtom, message.routerModels) + case "routerModels": { + const routerModels = message.routerModels as RouterModels | undefined + if (routerModels) { + set(updateRouterModelsAtom, routerModels) } break + } - case "profileDataResponse": + case "profileDataResponse": { set(setProfileLoadingAtom, false) - if (message.payload?.success) { - set(updateProfileDataAtom, message.payload.data) + const payload = message.payload as { success: boolean; data?: unknown; error?: string } | undefined + if (payload?.success) { + set(updateProfileDataAtom, payload.data as ProfileData) } else { - set(setProfileErrorAtom, message.payload?.error || "Failed to fetch profile") + set(setProfileErrorAtom, payload?.error || "Failed to fetch profile") } break + } - case "balanceDataResponse": + case "balanceDataResponse": { // Handle balance data response set(setBalanceLoadingAtom, false) - if (message.payload?.success) { - set(updateBalanceDataAtom, message.payload.data) + const payload = message.payload as { success: boolean; data?: unknown; error?: string } | undefined + if (payload?.success) { + set(updateBalanceDataAtom, payload.data as BalanceData) } else { - set(setBalanceErrorAtom, message.payload?.error || "Failed to fetch balance") + set(setBalanceErrorAtom, payload?.error || "Failed to fetch balance") } break + } - case "taskHistoryResponse": + case "taskHistoryResponse": { // Handle task history response set(taskHistoryLoadingAtom, false) - if (message.payload) { - const { historyItems, pageIndex, pageCount, requestId } = message.payload as any + const payload = message.payload as + | { + historyItems?: HistoryItem[] + pageIndex?: number + pageCount?: number + requestId?: string + } + | undefined + if (payload) { + const { historyItems, pageIndex, pageCount, requestId } = payload const data = { historyItems: historyItems || [], pageIndex: pageIndex || 0, @@ -188,14 +207,16 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension } else { set(taskHistoryErrorAtom, "Failed to fetch task history") // Reject any pending requests - if (message.payload?.requestId) { + const payloadWithRequestId = message.payload as { requestId?: string } | undefined + if (payloadWithRequestId?.requestId) { set(resolveTaskHistoryRequestAtom, { - requestId: message.payload.requestId, + requestId: payloadWithRequestId.requestId, error: "Failed to fetch task history", }) } } break + } case "action": // Action messages are typically handled by the UI diff --git a/cli/src/state/atoms/extension.ts b/cli/src/state/atoms/extension.ts index 5b4de27ca7e..45bb8202400 100644 --- a/cli/src/state/atoms/extension.ts +++ b/cli/src/state/atoms/extension.ts @@ -65,7 +65,7 @@ export const extensionModeAtom = atom("code") /** * Atom to hold custom modes */ -export const customModesAtom = atom([]) +export const customModesAtom = atom([]) /** * Atom to hold MCP servers configuration diff --git a/cli/src/state/atoms/history.ts b/cli/src/state/atoms/history.ts index 4662e280cf6..19b360c4692 100644 --- a/cli/src/state/atoms/history.ts +++ b/cli/src/state/atoms/history.ts @@ -116,7 +116,7 @@ export const loadHistoryAtom = atom(null, async (get, set) => { /** * Save history to disk */ -export const saveHistoryAtom = atom(null, async (get, set) => { +export const saveHistoryAtom = atom(null, async (get, _set) => { try { const data = get(historyDataAtom) await saveHistory(data) diff --git a/cli/src/state/atoms/keyboard.ts b/cli/src/state/atoms/keyboard.ts index 330d8435f14..36276cbd56d 100644 --- a/cli/src/state/atoms/keyboard.ts +++ b/cli/src/state/atoms/keyboard.ts @@ -2,7 +2,7 @@ * Jotai atoms for centralized keyboard event state management */ -import { atom } from "jotai" +import { atom, Getter, Setter, type Getter as _Getter, type Setter as _Setter } from "jotai" import type { Key, KeypressHandler } from "../../types/keyboard.js" import type { CommandSuggestion, ArgumentSuggestion, FileMentionSuggestion } from "../../services/autocomplete.js" import { @@ -389,7 +389,7 @@ function formatSuggestion( /** * Approval mode keyboard handler */ -function handleApprovalKeys(get: any, set: any, key: Key) { +function handleApprovalKeys(get: Getter, set: Setter, key: Key) { const selectedIndex = get(selectedIndexAtom) const options = get(approvalOptionsAtom) @@ -437,7 +437,7 @@ function handleApprovalKeys(get: any, set: any, key: Key) { /** * Followup mode keyboard handler */ -function handleFollowupKeys(get: any, set: any, key: Key): void { +function handleFollowupKeys(get: Getter, set: Setter, key: Key): void { const selectedIndex = get(selectedIndexAtom) const suggestions = get(followupSuggestionsAtom) @@ -495,7 +495,7 @@ function handleFollowupKeys(get: any, set: any, key: Key): void { /** * Autocomplete mode keyboard handler */ -function handleAutocompleteKeys(get: any, set: any, key: Key): void { +function handleAutocompleteKeys(get: Getter, set: Setter, key: Key): void { const selectedIndex = get(selectedIndexAtom) const commandSuggestions = get(suggestionsAtom) const argumentSuggestions = get(argumentSuggestionsAtom) @@ -593,7 +593,7 @@ function handleAutocompleteKeys(get: any, set: any, key: Key): void { * History mode keyboard handler * Handles navigation through command history */ -function handleHistoryKeys(get: any, set: any, key: Key): void { +function handleHistoryKeys(get: Getter, set: Setter, key: Key): void { switch (key.name) { case "up": { // Navigate to older command @@ -626,7 +626,7 @@ function handleHistoryKeys(get: any, set: any, key: Key): void { * Shell mode keyboard handler * Handles shell command input and execution using existing text buffer */ -async function handleShellKeys(get: any, set: any, key: Key): Promise { +async function handleShellKeys(get: Getter, set: Setter, key: Key): Promise { const currentInput = get(textBufferStringAtom) switch (key.name) { @@ -666,7 +666,7 @@ async function handleShellKeys(get: any, set: any, key: Key): Promise { * Unified text input keyboard handler * Handles both normal (single-line) and multiline text input */ -function handleTextInputKeys(get: any, set: any, key: Key) { +function handleTextInputKeys(get: Getter, set: Setter, key: Key) { // Check if we should enter history mode const isEmpty = get(textBufferIsEmptyAtom) const isInHistoryMode = get(historyModeAtom) @@ -786,7 +786,7 @@ function handleTextInputKeys(get: any, set: any, key: Key) { return } -function handleGlobalHotkeys(get: any, set: any, key: Key): boolean { +function handleGlobalHotkeys(get: Getter, set: Setter, key: Key): boolean { switch (key.name) { case "c": if (key.ctrl) { diff --git a/cli/src/state/atoms/ui.ts b/cli/src/state/atoms/ui.ts index cca086f50c8..f3da44a873d 100644 --- a/cli/src/state/atoms/ui.ts +++ b/cli/src/state/atoms/ui.ts @@ -486,7 +486,7 @@ export const hideAutocompleteAtom = atom(null, (get, set) => { * This atom is kept for backward compatibility but has no effect * @deprecated This atom is kept for backward compatibility but may be removed */ -export const showAutocompleteMenuAtom = atom(null, (get, set) => { +export const showAutocompleteMenuAtom = atom(null, (_get, _set) => { // No-op: autocomplete visibility is now derived from text buffer // Kept for backward compatibility }) diff --git a/cli/src/state/hooks/__tests__/hooks.test.tsx b/cli/src/state/hooks/__tests__/hooks.test.tsx index 81de8cc8033..8e80474cab1 100644 --- a/cli/src/state/hooks/__tests__/hooks.test.tsx +++ b/cli/src/state/hooks/__tests__/hooks.test.tsx @@ -45,13 +45,13 @@ describe("Hook Atoms", () => { { ts: Date.now(), type: "say", - say: "test", + say: "text", text: "Hello", }, { ts: Date.now() + 1000, type: "ask", - ask: "question", + ask: "followup", text: "What?", isAnswered: false, }, @@ -78,33 +78,31 @@ describe("Hook Atoms", () => { describe("Task Management Atoms", () => { const mockTask: HistoryItem = { + number: 1, id: "task-1", ts: Date.now(), task: "Test task", workspace: "/test", + totalCost: 0, + tokensIn: 0, + tokensOut: 0, } const mockTodos: TodoItem[] = [ { id: "todo-1", - text: "Todo 1", + content: "Todo 1", status: "pending", - createdAt: Date.now(), - updatedAt: Date.now(), }, { id: "todo-2", - text: "Todo 2", + content: "Todo 2", status: "in_progress", - createdAt: Date.now(), - updatedAt: Date.now(), }, { id: "todo-3", - text: "Todo 3", + content: "Todo 3", status: "completed", - createdAt: Date.now(), - updatedAt: Date.now(), }, ] diff --git a/cli/src/state/hooks/__tests__/useTerminal.test.ts b/cli/src/state/hooks/__tests__/useTerminal.test.ts index 2001fdf2162..0338da6882f 100644 --- a/cli/src/state/hooks/__tests__/useTerminal.test.ts +++ b/cli/src/state/hooks/__tests__/useTerminal.test.ts @@ -32,22 +32,24 @@ describe("useTerminal", () => { }) // Mock process.stdout.on and off - vi.spyOn(process.stdout, "on").mockImplementation((event: string, listener: any) => { + vi.spyOn(process.stdout, "on").mockImplementation((event: string, listener: (...args: unknown[]) => void) => { if (event === "resize") { resizeListeners.push(listener) } return process.stdout }) - vi.spyOn(process.stdout, "off").mockImplementation((event: string | symbol, listener: any) => { - if (event === "resize") { - const index = resizeListeners.indexOf(listener) - if (index > -1) { - resizeListeners.splice(index, 1) + vi.spyOn(process.stdout, "off").mockImplementation( + (event: string | symbol, listener: (...args: unknown[]) => void) => { + if (event === "resize") { + const index = resizeListeners.indexOf(listener) + if (index > -1) { + resizeListeners.splice(index, 1) + } } - } - return process.stdout - }) + return process.stdout + }, + ) vi.spyOn(process.stdout, "write").mockImplementation(() => true) }) diff --git a/cli/src/state/hooks/useCommandContext.ts b/cli/src/state/hooks/useCommandContext.ts index 2a422a2a6b1..e5e9368d41a 100644 --- a/cli/src/state/hooks/useCommandContext.ts +++ b/cli/src/state/hooks/useCommandContext.ts @@ -7,6 +7,8 @@ import { useSetAtom, useAtomValue } from "jotai" import { useCallback } from "react" import type { CommandContext } from "../../commands/core/types.js" import type { CliMessage } from "../../types/cli.js" +import type { ProviderConfig } from "../../config/types.js" +import type { ExtensionMessage } from "../../types/messages.js" import { addMessageAtom, clearMessagesAtom, @@ -37,7 +39,7 @@ const TERMINAL_CLEAR_DELAY_MS = 500 export type CommandContextFactory = ( input: string, args: string[], - options: Record, + options: Record, onExit: () => void, ) => CommandContext @@ -85,7 +87,7 @@ export function useCommandContext(): UseCommandContextReturn { const routerModels = useAtomValue(routerModelsAtom) const currentProvider = useAtomValue(providerAtom) const extensionState = useAtomValue(extensionStateAtom) - const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || "" + const kilocodeDefaultModel = (extensionState?.kilocodeDefaultModel as string) || "" const isParallelMode = useAtomValue(isParallelModeAtom) const config = useAtomValue(configAtom) const chatMessages = useAtomValue(chatMessagesAtom) @@ -111,14 +113,19 @@ export function useCommandContext(): UseCommandContextReturn { // Create the factory function const createContext = useCallback( - (input: string, args: string[], options: Record, onExit: () => void): CommandContext => { + ( + input: string, + args: string[], + options: Record, + onExit: () => void, + ): CommandContext => { return { input, args, options, config, - sendMessage: async (message: any) => { - await sendMessage(message) + sendMessage: async (message: unknown) => { + await sendMessage(message as Parameters[0]) }, addMessage: (message: CliMessage) => { addMessage(message) @@ -174,7 +181,7 @@ export function useCommandContext(): UseCommandContextReturn { await refreshRouterModels() }, // Provider update function for teams command - updateProvider: async (providerId: string, updates: any) => { + updateProvider: async (providerId: string, updates: Partial) => { await updateProvider(providerId, updates) }, // Profile data context @@ -193,7 +200,7 @@ export function useCommandContext(): UseCommandContextReturn { nextTaskHistoryPage, previousTaskHistoryPage, sendWebviewMessage: sendMessage, - chatMessages, + chatMessages: chatMessages as unknown as ExtensionMessage[], } }, [ diff --git a/cli/src/state/hooks/useCommandInput.ts b/cli/src/state/hooks/useCommandInput.ts index a3199173232..14b14631c8a 100644 --- a/cli/src/state/hooks/useCommandInput.ts +++ b/cli/src/state/hooks/useCommandInput.ts @@ -11,6 +11,7 @@ import type { FileMentionSuggestion, FileMentionContext, } from "../../services/autocomplete.js" +import type { ExtensionMessage } from "../../types/messages.js" import { getSuggestions, getArgumentSuggestions, @@ -45,7 +46,7 @@ import { import { shellModeActiveAtom } from "../atoms/shell.js" import { textBufferStringAtom, textBufferCursorAtom } from "../atoms/textBuffer.js" import { routerModelsAtom, extensionStateAtom } from "../atoms/extension.js" -import { providerAtom, updateProviderAtom } from "../atoms/config.js" +import { configAtom, providerAtom, updateProviderAtom } from "../atoms/config.js" import { requestRouterModelsAtom } from "../atoms/actions.js" import { profileDataAtom, profileLoadingAtom } from "../atoms/profile.js" import { taskHistoryDataAtom } from "../atoms/taskHistory.js" @@ -163,10 +164,11 @@ export function useCommandInput(): UseCommandInputReturn { const selectedSuggestion = useAtomValue(getSelectedSuggestionAtom) // Get command context for autocomplete + const config = useAtomValue(configAtom) const routerModels = useAtomValue(routerModelsAtom) const currentProvider = useAtomValue(providerAtom) const extensionState = useAtomValue(extensionStateAtom) - const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || "" + const kilocodeDefaultModel = (extensionState?.kilocodeDefaultModel as string | undefined) || "" const profileData = useAtomValue(profileDataAtom) const profileLoading = useAtomValue(profileLoadingAtom) const taskHistoryData = useAtomValue(taskHistoryDataAtom) @@ -266,12 +268,14 @@ export function useCommandInput(): UseCommandInputReturn { } else if (state.type === "argument") { // Create command context for argument providers const commandContext = { + config, routerModels, currentProvider: currentProvider || null, kilocodeDefaultModel, profileData, profileLoading, taskHistoryData, + chatMessages: [] as ExtensionMessage[], updateProviderModel: async (modelId: string) => { if (!currentProvider) { throw new Error("No provider configured") @@ -304,6 +308,7 @@ export function useCommandInput(): UseCommandInputReturn { setFileMentionSuggestionsAction, setFileMentionContextAction, clearFileMentionAction, + config, routerModels, currentProvider, kilocodeDefaultModel, diff --git a/cli/src/state/hooks/useModelSelection.ts b/cli/src/state/hooks/useModelSelection.ts index ab2ebf8fca7..ad0a8dbab54 100644 --- a/cli/src/state/hooks/useModelSelection.ts +++ b/cli/src/state/hooks/useModelSelection.ts @@ -37,7 +37,7 @@ export interface UseModelSelectionReturn { /** Check if a specific provider is configured */ isProviderConfigured: (provider: string) => boolean /** Get model info by ID from router models */ - getModelInfo: (modelId: string) => any | null + getModelInfo: (modelId: string) => unknown | null } /** @@ -127,7 +127,7 @@ export function useModelSelection(): UseModelSelectionReturn { // Search through all routers for (const [routerName, models] of Object.entries(routerModels)) { if (routerName === "default") continue - const modelRecord = models as Record + const modelRecord = models as Record if (modelRecord[modelId]) { return modelRecord[modelId] } diff --git a/cli/src/state/hooks/useProfile.ts b/cli/src/state/hooks/useProfile.ts index 729fa5872b1..7c960178c84 100644 --- a/cli/src/state/hooks/useProfile.ts +++ b/cli/src/state/hooks/useProfile.ts @@ -75,9 +75,10 @@ export function useProfile(): UseProfileReturn { const { sendMessage } = useWebviewMessage() // Get current organization - const currentOrganization = currentProvider?.kilocodeOrganizationId - ? getCurrentOrganization(currentProvider.kilocodeOrganizationId) - : null + const currentOrganization = + currentProvider?.kilocodeOrganizationId && typeof currentProvider.kilocodeOrganizationId === "string" + ? getCurrentOrganization(currentProvider.kilocodeOrganizationId) + : null /** * Fetch profile data from the extension diff --git a/cli/src/state/hooks/useTaskHistory.ts b/cli/src/state/hooks/useTaskHistory.ts index a4bfaf7a0dc..67035c8b12c 100644 --- a/cli/src/state/hooks/useTaskHistory.ts +++ b/cli/src/state/hooks/useTaskHistory.ts @@ -19,6 +19,7 @@ import { } from "../atoms/taskHistory.js" import { extensionServiceAtom } from "../atoms/service.js" import { logs } from "../../services/logs.js" +import type { TaskHistoryRequestPayload } from "../../types/messages.js" export function useTaskHistory() { const service = useAtomValue(extensionServiceAtom) @@ -42,16 +43,19 @@ export function useTaskHistory() { try { // Send task history request to extension + const payload: TaskHistoryRequestPayload = { + requestId: Date.now().toString(), + workspace: filters.workspace, + sort: filters.sort, + favoritesOnly: filters.favoritesOnly, + pageIndex, + } + if (filters.search) { + payload.search = filters.search + } await service.sendWebviewMessage({ type: "taskHistoryRequest", - payload: { - requestId: Date.now().toString(), - workspace: filters.workspace, - sort: filters.sort, - favoritesOnly: filters.favoritesOnly, - pageIndex, - search: filters.search, - }, + payload, }) } catch (err) { logs.error("fetchTaskHistory error:", "useTaskHistory", { error: err }) @@ -87,17 +91,20 @@ export function useTaskHistory() { addPendingRequest({ requestId, resolve, reject, timeout }) // Send the request with updated filters + const payload: TaskHistoryRequestPayload = { + requestId, + workspace: updatedFilters.workspace, + sort: updatedFilters.sort, + favoritesOnly: updatedFilters.favoritesOnly, + pageIndex: 0, // Filters reset to page 0 + } + if (updatedFilters.search) { + payload.search = updatedFilters.search + } service .sendWebviewMessage({ type: "taskHistoryRequest", - payload: { - requestId, - workspace: updatedFilters.workspace, - sort: updatedFilters.sort, - favoritesOnly: updatedFilters.favoritesOnly, - pageIndex: 0, // Filters reset to page 0 - search: updatedFilters.search, - }, + payload, }) .catch((err) => { removePendingRequest(requestId) @@ -134,17 +141,20 @@ export function useTaskHistory() { addPendingRequest({ requestId, resolve, reject, timeout }) // Send the request + const payload: TaskHistoryRequestPayload = { + requestId, + workspace: filters.workspace, + sort: filters.sort, + favoritesOnly: filters.favoritesOnly, + pageIndex: newPageIndex, + } + if (filters.search) { + payload.search = filters.search + } service .sendWebviewMessage({ type: "taskHistoryRequest", - payload: { - requestId, - workspace: filters.workspace, - sort: filters.sort, - favoritesOnly: filters.favoritesOnly, - pageIndex: newPageIndex, - search: filters.search, - }, + payload, }) .catch((err) => { removePendingRequest(requestId) diff --git a/cli/src/state/hooks/useWebviewMessage.ts b/cli/src/state/hooks/useWebviewMessage.ts index 8b804f87c41..2d5980f01cb 100644 --- a/cli/src/state/hooks/useWebviewMessage.ts +++ b/cli/src/state/hooks/useWebviewMessage.ts @@ -5,7 +5,7 @@ import { useSetAtom } from "jotai" import { useCallback, useMemo } from "react" -import type { WebviewMessage } from "../../types/messages.js" +import type { WebviewMessage, ClineAskResponse } from "../../types/messages.js" import { sendWebviewMessageAtom, sendTaskAtom, @@ -42,8 +42,8 @@ export interface SendTaskParams { * Parameters for sending an ask response */ export interface SendAskResponseParams { - /** The response text */ - response?: string + /** The response type */ + response?: ClineAskResponse /** The action to take */ action?: string /** Additional text */ @@ -57,7 +57,7 @@ export interface SendAskResponseParams { */ export interface RespondToToolParams { /** Whether to approve or reject */ - response: "yesButtonTapped" | "noButtonTapped" + response: "yesButtonClicked" | "noButtonClicked" /** Optional feedback text */ text?: string /** Optional images */ @@ -87,7 +87,7 @@ export interface UseWebviewMessageReturn { /** Respond to a tool use request */ respondToTool: (params: RespondToToolParams) => Promise /** Send API configuration */ - sendApiConfiguration: (config: any) => Promise + sendApiConfiguration: (config: unknown) => Promise /** Send custom instructions */ sendCustomInstructions: (instructions: string) => Promise /** Send always allow setting */ @@ -200,8 +200,8 @@ export function useWebviewMessage(): UseWebviewMessageReturn { ) const sendApiConfiguration = useCallback( - async (config: any) => { - await sendApiConfigurationAction(config) + async (config: unknown) => { + await sendApiConfigurationAction(config as Parameters[0]) }, [sendApiConfigurationAction], ) diff --git a/cli/src/types/cli.ts b/cli/src/types/cli.ts index daf4e5ddd40..cf24084c250 100644 --- a/cli/src/types/cli.ts +++ b/cli/src/types/cli.ts @@ -13,11 +13,12 @@ export interface WelcomeMessageOptions { export interface CliMessage { id: string - type: "user" | "assistant" | "system" | "error" | "welcome" | "empty" + type: "user" | "assistant" | "system" | "error" | "welcome" | "empty" | "requestCheckpointRestoreApproval" content: string ts: number partial?: boolean | undefined metadata?: { welcomeOptions?: WelcomeMessageOptions | undefined } + payload?: unknown } diff --git a/cli/src/types/keyboard.ts b/cli/src/types/keyboard.ts index 66d6d55934c..cb07ad37d91 100644 --- a/cli/src/types/keyboard.ts +++ b/cli/src/types/keyboard.ts @@ -22,6 +22,17 @@ export interface Key { kittyProtocol?: boolean } +/** + * Represents a key object from Node's readline keypress event + */ +export interface ReadlineKey { + name?: string + sequence: string + ctrl?: boolean + meta?: boolean + shift?: boolean +} + /** * Handler function type for key events */ diff --git a/cli/src/types/messages.ts b/cli/src/types/messages.ts index 214cd0c3ba9..1eeced51d96 100644 --- a/cli/src/types/messages.ts +++ b/cli/src/types/messages.ts @@ -1,369 +1,58 @@ -// Local type definitions for CLI application -// These mirror the types from the main extension but are defined locally to avoid import issues - -// Import model-related types +// ============================================ +// SHARED TYPES - Import from @roo-code/types +// ============================================ +import type { + ProviderSettings, + ProviderSettingsEntry, + ProviderName, + HistoryItem, + ModeConfig, + TodoItem, + ClineMessage, +} from "@roo-code/types" + +// ============================================ +// SHARED TYPES - Import from src/shared +// ============================================ +export type { + WebviewMessage, + MaybeTypedWebviewMessage, + UpdateGlobalStateMessage, + ClineAskResponse, + TaskHistoryRequestPayload, +} from "@roo/WebviewMessage" + +import type { McpServer, McpTool, McpResource } from "@roo/mcp" +export type { McpServer, McpTool, McpResource } + +// ============================================ +// MODEL TYPES - Import from constants +// ============================================ import type { RouterName, ModelInfo, ModelRecord, RouterModels } from "../constants/providers/models.js" -// Re-export for external use +// ============================================ +// RE-EXPORTS for convenience +// ============================================ +export type { ProviderSettings, ProviderSettingsEntry, ProviderName, HistoryItem, ModeConfig, TodoItem } + +// Alias ClineMessage as ExtensionChatMessage for backward compatibility +export type ExtensionChatMessage = ClineMessage + +// Re-export model types export type { RouterName, ModelInfo, ModelRecord, RouterModels } +// ============================================ +// CLI-SPECIFIC TYPES (Keep these) +// ============================================ export interface ExtensionMessage { type: string action?: string text?: string - state?: any - images?: string[] - chatMessages?: any - values?: Record - [key: string]: any -} - -export interface WebviewMessage { - type: string - text?: string - images?: string[] - bool?: boolean - value?: number - commands?: string[] - apiConfiguration?: any - mode?: string - values?: Record - askResponse?: string - terminalOperation?: string - context?: string - invoke?: string - action?: string - [key: string]: any -} - -export interface ExtensionChatMessage { - ts: number - type: "ask" | "say" - ask?: string - say?: string - text?: string + state?: ExtensionState images?: string[] - partial?: boolean - isProtected?: boolean - isAnswered?: boolean - checkpoint?: any - metadata?: any -} - -export interface HistoryItem { - id: string - ts: number - task: string - workspace: string - mode?: string - isFavorited?: boolean - fileNotfound?: boolean - rootTaskId?: string - parentTaskId?: string - number?: number -} - -// Provider Names -export type ProviderName = - | "anthropic" - | "claude-code" - | "glama" - | "openrouter" - | "bedrock" - | "vertex" - | "openai" - | "ollama" - | "vscode-lm" - | "lmstudio" - | "gemini" - | "openai-native" - | "mistral" - | "moonshot" - | "deepseek" - | "deepinfra" - | "doubao" - | "qwen-code" - | "unbound" - | "requesty" - | "human-relay" - | "fake-ai" - | "xai" - | "groq" - | "chutes" - | "litellm" - | "kilocode" - | "gemini-cli" - | "virtual-quota-fallback" - | "huggingface" - | "cerebras" - | "sambanova" - | "zai" - | "fireworks" - | "featherless" - | "io-intelligence" - | "roo" - | "vercel-ai-gateway" - | "minimax" - | "ovhcloud" - -// Provider Settings Entry for profile metadata -export interface ProviderSettingsEntry { - id: string - name: string - apiProvider?: ProviderName - modelId?: string -} - -// Comprehensive Provider Settings -export interface ProviderSettings { - // Base settings - apiProvider?: ProviderName - includeMaxTokens?: boolean - diffEnabled?: boolean - todoListEnabled?: boolean - fuzzyMatchThreshold?: number - modelTemperature?: number | null - rateLimitSeconds?: number - consecutiveMistakeLimit?: number - enableReasoningEffort?: boolean - reasoningEffort?: "low" | "medium" | "high" - modelMaxTokens?: number - modelMaxThinkingTokens?: number - verbosity?: "concise" | "normal" | "verbose" - - // Common model fields - apiModelId?: string - - // Anthropic - apiKey?: string - anthropicBaseUrl?: string - anthropicUseAuthToken?: boolean - anthropicBeta1MContext?: boolean - - // Claude Code - claudeCodePath?: string - claudeCodeMaxOutputTokens?: number - - // Glama - glamaModelId?: string - glamaApiKey?: string - - // OpenRouter - openRouterApiKey?: string - openRouterModelId?: string - openRouterBaseUrl?: string - openRouterSpecificProvider?: string - openRouterUseMiddleOutTransform?: boolean - openRouterProviderDataCollection?: "allow" | "deny" - openRouterProviderSort?: "price" | "throughput" | "latency" - - // Bedrock - awsAccessKey?: string - awsSecretKey?: string - awsSessionToken?: string - awsRegion?: string - awsUseCrossRegionInference?: boolean - awsUsePromptCache?: boolean - awsProfile?: string - awsUseProfile?: boolean - awsApiKey?: string - awsUseApiKey?: boolean - awsCustomArn?: string - awsModelContextWindow?: number - awsBedrockEndpointEnabled?: boolean - awsBedrockEndpoint?: string - awsBedrock1MContext?: boolean - - // Vertex - vertexKeyFile?: string - vertexJsonCredentials?: string - vertexProjectId?: string - vertexRegion?: string - enableUrlContext?: boolean - enableGrounding?: boolean - - // OpenAI Compatible - openAiBaseUrl?: string - openAiApiKey?: string - openAiLegacyFormat?: boolean - openAiR1FormatEnabled?: boolean - openAiModelId?: string - openAiUseAzure?: boolean - azureApiVersion?: string - openAiStreamingEnabled?: boolean - openAiHeaders?: Record - - // OpenAI Native - openAiNativeApiKey?: string - openAiNativeBaseUrl?: string - openAiNativeServiceTier?: "default" | "flex" | "priority" - - // Ollama - ollamaModelId?: string - ollamaBaseUrl?: string - ollamaApiKey?: string - - // VS Code LM - vsCodeLmModelSelector?: { - vendor?: string - family?: string - version?: string - id?: string - } - - // LM Studio - lmStudioModelId?: string - lmStudioBaseUrl?: string - lmStudioDraftModelId?: string - lmStudioSpeculativeDecodingEnabled?: boolean - - // Gemini - geminiApiKey?: string - googleGeminiBaseUrl?: string - - // Gemini CLI - geminiCliOAuthPath?: string - geminiCliProjectId?: string - - // Mistral - mistralApiKey?: string - mistralCodestralUrl?: string - - // DeepSeek - deepSeekBaseUrl?: string - deepSeekApiKey?: string - - // DeepInfra - deepInfraBaseUrl?: string - deepInfraApiKey?: string - deepInfraModelId?: string - - // Doubao - doubaoBaseUrl?: string - doubaoApiKey?: string - - // Moonshot - moonshotBaseUrl?: "https://api.moonshot.ai/v1" | "https://api.moonshot.cn/v1" - moonshotApiKey?: string - - // Unbound - unboundApiKey?: string - unboundModelId?: string - - // Requesty - requestyBaseUrl?: string - requestyApiKey?: string - requestyModelId?: string - - // XAI - xaiApiKey?: string - - // Groq - groqApiKey?: string - - // Hugging Face - huggingFaceApiKey?: string - huggingFaceModelId?: string - huggingFaceInferenceProvider?: string - - // Chutes - chutesApiKey?: string - - // LiteLLM - litellmBaseUrl?: string - litellmApiKey?: string - litellmModelId?: string - litellmUsePromptCache?: boolean - - // Cerebras - cerebrasApiKey?: string - - // SambaNova - sambaNovaApiKey?: string - - // Kilocode - kilocodeToken?: string - kilocodeOrganizationId?: string - kilocodeModel?: string - kilocodeTesterWarningsDisabledUntil?: number - - // Virtual Quota Fallback - profiles?: Array<{ - profileName?: string - profileId?: string - profileLimits?: { - tokensPerMinute?: number - tokensPerHour?: number - tokensPerDay?: number - requestsPerMinute?: number - requestsPerHour?: number - requestsPerDay?: number - } - }> - - // ZAI - zaiApiKey?: string - zaiApiLine?: "international_coding" | "international" | "china_coding" | "china" - - // Fireworks - fireworksApiKey?: string - - // Featherless - featherlessApiKey?: string - - // IO Intelligence - ioIntelligenceModelId?: string - ioIntelligenceApiKey?: string - - // Qwen Code - qwenCodeOauthPath?: string - - // Vercel AI Gateway - vercelAiGatewayApiKey?: string - vercelAiGatewayModelId?: string - - // MiniMax AI - minimaxBaseUrl?: "https://api.minimax.io/anthropic" | "https://api.minimaxi.com/anthropic" - minimaxApiKey?: string - - // OVHcloud AI Endpoints - ovhCloudAiEndpointsApiKey?: string - ovhCloudAiEndpointsModelId?: string - - // Allow additional fields for extensibility - [key: string]: any -} - -export interface TodoItem { - id: string - text: string - status: "pending" | "in_progress" | "completed" - createdAt: number - updatedAt: number -} - -export interface McpServer { - name: string - command: string - args?: string[] - env?: Record - disabled?: boolean - alwaysAllow?: boolean - tools?: McpTool[] - resources?: McpResource[] -} - -export interface McpTool { - name: string - description?: string - alwaysAllow?: boolean - enabledForPrompt?: boolean -} - -export interface McpResource { - uri: string - name?: string - description?: string + chatMessages?: ExtensionChatMessage[] + values?: Record + [key: string]: unknown } // Organization Allow List for provider validation @@ -378,6 +67,7 @@ export interface OrganizationAllowList { > } +// CLI-specific ExtensionState export interface ExtensionState { version: string apiConfiguration: ProviderSettings @@ -388,7 +78,7 @@ export interface ExtensionState { currentTaskItem?: HistoryItem currentTaskTodos?: TodoItem[] mode: string - customModes: any[] + customModes: ModeConfig[] taskHistoryFullLength: number taskHistoryVersion: number mcpServers?: McpServer[] @@ -397,16 +87,7 @@ export interface ExtensionState { cwd?: string organizationAllowList?: OrganizationAllowList routerModels?: RouterModels - [key: string]: any + [key: string]: unknown } export type Mode = string - -export interface ModeConfig { - slug: string - name: string - description?: string - systemPrompt?: string - rules?: string[] - source?: "global" | "project" -} diff --git a/cli/src/ui/components/StatusBar.tsx b/cli/src/ui/components/StatusBar.tsx index fa0f4d9b7bf..585caa213cb 100644 --- a/cli/src/ui/components/StatusBar.tsx +++ b/cli/src/ui/components/StatusBar.tsx @@ -24,6 +24,7 @@ import { type RouterModels, } from "../../constants/providers/models.js" import type { ProviderSettings } from "../../types/messages.js" +import type { ProviderConfig } from "../../config/types.js" import path from "path" import { isGitWorktree } from "../../utils/git.js" @@ -39,9 +40,10 @@ function getModelDisplayName(apiConfig: ProviderSettings | null, routerModels: R // Get current model ID const currentModelId = getCurrentModelId({ providerConfig: { - provider: apiConfig.apiProvider, + id: "default", + provider: apiConfig.apiProvider || "", ...apiConfig, - } as any, + } as ProviderConfig, routerModels, kilocodeDefaultModel: apiConfig.kilocodeModel || "", }) diff --git a/cli/src/ui/components/__tests__/StatusBar.test.tsx b/cli/src/ui/components/__tests__/StatusBar.test.tsx index 5591a053def..7a81b57bda7 100644 --- a/cli/src/ui/components/__tests__/StatusBar.test.tsx +++ b/cli/src/ui/components/__tests__/StatusBar.test.tsx @@ -25,7 +25,7 @@ describe("StatusBar", () => { vi.clearAllMocks() // Setup default mock implementations - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return "/home/user/kilocode" if (atom === atoms.isParallelModeAtom) return false if (atom === atoms.extensionModeAtom) return "code" @@ -116,7 +116,7 @@ describe("StatusBar", () => { }) it("should handle missing cwd", () => { - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return null if (atom === atoms.extensionModeAtom) return "code" if (atom === atoms.apiConfigurationAtom) @@ -136,7 +136,7 @@ describe("StatusBar", () => { }) it("should handle missing api config", () => { - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return "/home/user/project" if (atom === atoms.extensionModeAtom) return "architect" if (atom === atoms.apiConfigurationAtom) return null @@ -150,7 +150,7 @@ describe("StatusBar", () => { }) it("should capitalize mode name", () => { - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return "/home/user/project" if (atom === atoms.extensionModeAtom) return "architect" if (atom === atoms.apiConfigurationAtom) @@ -187,7 +187,7 @@ describe("StatusBar", () => { }) it("should render without errors with different modes", () => { - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return "/home/user/test-project" if (atom === atoms.extensionModeAtom) return "debug" if (atom === atoms.apiConfigurationAtom) @@ -208,7 +208,7 @@ describe("StatusBar", () => { }) describe("parallel mode", () => { - let isGitWorktreeMock: any + let isGitWorktreeMock: ReturnType beforeEach(async () => { const gitModule = await import("../../../utils/git.js") @@ -233,7 +233,7 @@ describe("StatusBar", () => { // Mock process.cwd() to return the actual project directory process.cwd = vi.fn(() => "/home/user/kilocode") - vi.mocked(useAtomValue).mockImplementation((atom: any) => { + vi.mocked(useAtomValue).mockImplementation((atom: unknown) => { if (atom === atoms.cwdAtom) return "/tmp/worktree/kilocode-task-123" if (atom === atoms.isParallelModeAtom) return true if (atom === atoms.extensionModeAtom) return "code" diff --git a/cli/src/ui/messages/extension/SayMessageRouter.tsx b/cli/src/ui/messages/extension/SayMessageRouter.tsx index 785b8fad5e6..8db3563e902 100644 --- a/cli/src/ui/messages/extension/SayMessageRouter.tsx +++ b/cli/src/ui/messages/extension/SayMessageRouter.tsx @@ -1,8 +1,6 @@ import React from "react" import { Box, Text } from "ink" import type { MessageComponentProps } from "./types.js" -import { parseToolData } from "./utils.js" -import { ToolRouter } from "./tools/ToolRouter.js" import { useTheme } from "../../../state/hooks/useTheme.js" import { SayTextMessage, @@ -94,15 +92,6 @@ export const SayMessageRouter: React.FC = ({ message }) = case "user_edit_todos": return - case "tool": { - const toolData = parseToolData(message) - if (toolData) { - return - } - // Fallback to default if tool data can't be parsed - return - } - case "image": return diff --git a/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx b/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx index 2caec6b6950..b8d8114ec07 100644 --- a/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx +++ b/cli/src/ui/messages/extension/__tests__/ExtensionMessageRow.test.tsx @@ -6,10 +6,10 @@ */ import { describe, it, expect, vi, beforeEach } from "vitest" -import React from "react" import { render } from "ink-testing-library" import { ExtensionMessageRow } from "../ExtensionMessageRow.js" import type { ExtensionChatMessage } from "../../../../types/messages.js" +import type { ClineAsk, ClineSay } from "@roo-code/types" // Mock the logs service vi.mock("../../../../services/logs.js", () => ({ @@ -109,7 +109,7 @@ describe("ExtensionMessageRow", () => { const message: ExtensionChatMessage = { ts: Date.now(), type: "ask", - ask: "unknown_type" as any, + ask: "followup" as unknown as ClineAsk, // Use valid type but test unknown handling text: "Unknown ask type", } @@ -202,7 +202,7 @@ describe("ExtensionMessageRow", () => { const message: ExtensionChatMessage = { ts: Date.now(), type: "say", - say: "unknown_type" as any, + say: "text" as unknown as ClineSay, // Use valid type but test unknown handling text: "Unknown say type", } @@ -218,7 +218,7 @@ describe("ExtensionMessageRow", () => { it("should show fallback for completely unknown message type", () => { const message: ExtensionChatMessage = { ts: Date.now(), - type: "unknown" as any, + type: "unknown" as unknown as ExtensionChatMessage["type"], text: "Unknown message", } @@ -399,7 +399,7 @@ describe("ExtensionMessageRow", () => { const message: ExtensionChatMessage = { ts: Date.now(), type: "say", - say: "tool", + say: "user_feedback_diff", text: JSON.stringify({ tool: "editedExistingFile", path: "src/test.ts", diff --git a/cli/src/ui/messages/extension/tools/ToolEditedExistingFileMessage.tsx b/cli/src/ui/messages/extension/tools/ToolEditedExistingFileMessage.tsx index 15d8b096693..b1ce2f93745 100644 --- a/cli/src/ui/messages/extension/tools/ToolEditedExistingFileMessage.tsx +++ b/cli/src/ui/messages/extension/tools/ToolEditedExistingFileMessage.tsx @@ -1,6 +1,6 @@ import React from "react" import { Box, Text } from "ink" -import type { ToolMessageProps } from "../types.js" +import type { ToolMessageProps, BatchDiffItem } from "../types.js" import { getToolIcon, formatFilePath, truncateText } from "../utils.js" import { useTheme } from "../../../../state/hooks/useTheme.js" import { getBoxWidth } from "../../../utils/width.js" @@ -28,7 +28,7 @@ export const ToolEditedExistingFileMessage: React.FC = ({ tool - {toolData.batchDiffs!.map((batchDiff: any, index: number) => ( + {toolData.batchDiffs!.map((batchDiff: BatchDiffItem, index: number) => ( {formatFilePath(batchDiff.path || "")} {batchDiff.isProtected && ( @@ -98,7 +98,7 @@ export const ToolEditedExistingFileMessage: React.FC = ({ tool )} - {toolData.fastApplyResult && ( + {!!toolData.fastApplyResult && ( ✓ Fast apply diff --git a/cli/src/ui/messages/extension/tools/ToolNewFileCreatedMessage.tsx b/cli/src/ui/messages/extension/tools/ToolNewFileCreatedMessage.tsx index 9ac63e9f213..363094305c3 100644 --- a/cli/src/ui/messages/extension/tools/ToolNewFileCreatedMessage.tsx +++ b/cli/src/ui/messages/extension/tools/ToolNewFileCreatedMessage.tsx @@ -55,13 +55,13 @@ export const ToolNewFileCreatedMessage: React.FC = ({ toolData - {toolData.fastApplyResult && ( + {toolData.fastApplyResult && typeof toolData.fastApplyResult === "object" ? ( ✓ Fast apply - )} + ) : null} ) } diff --git a/cli/src/ui/messages/extension/types.ts b/cli/src/ui/messages/extension/types.ts index 3efa25dbbbc..08226874716 100644 --- a/cli/src/ui/messages/extension/types.ts +++ b/cli/src/ui/messages/extension/types.ts @@ -14,6 +14,15 @@ export interface ToolMessageProps extends MessageComponentProps { toolData: ToolData } +/** + * Batch diff item structure + */ +export interface BatchDiffItem { + path: string + isProtected?: boolean + diff?: string +} + /** * Parsed tool data structure */ @@ -26,7 +35,7 @@ export interface ToolData { isProtected?: boolean isOutsideWorkspace?: boolean batchFiles?: Array<{ path: string }> - batchDiffs?: Array + batchDiffs?: Array lineNumber?: number regex?: string filePattern?: string @@ -38,7 +47,7 @@ export interface ToolData { description?: string source?: string additionalFileCount?: number - fastApplyResult?: any + fastApplyResult?: unknown } /** @@ -59,7 +68,7 @@ export interface McpServerData { toolName?: string arguments?: string uri?: string - response?: any + response?: unknown } /** diff --git a/cli/src/ui/messages/extension/utils.ts b/cli/src/ui/messages/extension/utils.ts index 5d9455230ba..6fc4b9589b3 100644 --- a/cli/src/ui/messages/extension/utils.ts +++ b/cli/src/ui/messages/extension/utils.ts @@ -5,7 +5,7 @@ import type { ToolData, McpServerData, FollowUpData, ApiReqInfo, ImageData } fro /** * Parse JSON from message text safely */ -export function parseMessageJson(text?: string): T | null { +export function parseMessageJson(text?: string): T | null { if (!text) return null try { return JSON.parse(text) as T diff --git a/cli/src/ui/messages/utils/messageCompletion.ts b/cli/src/ui/messages/utils/messageCompletion.ts index cf4704ce51e..c41ae695215 100644 --- a/cli/src/ui/messages/utils/messageCompletion.ts +++ b/cli/src/ui/messages/utils/messageCompletion.ts @@ -120,7 +120,7 @@ export function splitMessages(messages: UnifiedMessage[]): { const deduplicatedMessages = deduplicateCheckpointMessages(messages) let lastCompleteIndex = -1 - const incompleteReasons: Array<{ index: number; reason: string; message: any }> = [] + const incompleteReasons: Array<{ index: number; reason: string; message: unknown }> = [] // Find the last consecutive index where all messages up to that point are complete for (let i = 0; i < deduplicatedMessages.length; i++) { diff --git a/cli/src/ui/providers/KeyboardProvider.tsx b/cli/src/ui/providers/KeyboardProvider.tsx index c933416969c..b8a203d6eaa 100644 --- a/cli/src/ui/providers/KeyboardProvider.tsx +++ b/cli/src/ui/providers/KeyboardProvider.tsx @@ -9,7 +9,7 @@ import { useSetAtom, useAtomValue } from "jotai" import { useStdin } from "ink" import readline from "node:readline" import { PassThrough } from "node:stream" -import type { KeyboardProviderConfig } from "../../types/keyboard.js" +import type { KeyboardProviderConfig, ReadlineKey } from "../../types/keyboard.js" import { logs } from "../../services/logs.js" import { broadcastKeyEventAtom, @@ -162,7 +162,7 @@ export function KeyboardProvider({ children, config = {} }: KeyboardProviderProp readline.emitKeypressEvents(keypressStream, rl) // Handle keypress events from readline - const handleKeypress = (_: unknown, key: any) => { + const handleKeypress = (_: unknown, key: ReadlineKey) => { if (!key) return // Parse the key diff --git a/cli/src/ui/utils/keyParsing.ts b/cli/src/ui/utils/keyParsing.ts index aca13e8f8e2..80c5e86fd79 100644 --- a/cli/src/ui/utils/keyParsing.ts +++ b/cli/src/ui/utils/keyParsing.ts @@ -3,7 +3,7 @@ * Handles Kitty protocol, legacy ANSI sequences, and special key combinations */ -import type { Key } from "../../types/keyboard.js" +import type { Key, ReadlineKey } from "../../types/keyboard.js" import { ESC, CSI, @@ -341,7 +341,7 @@ export function mapAltKeyCharacter(char: string): string | null { /** * Parse a simple key from readline's keypress event */ -export function parseReadlineKey(key: any): Key { +export function parseReadlineKey(key: ReadlineKey): Key { // Handle the key object from readline const keyName = key.name || (key.sequence.length === 1 ? key.sequence : "") diff --git a/cli/src/ui/utils/terminalCapabilities.ts b/cli/src/ui/utils/terminalCapabilities.ts index d11ffad7cb1..06429ce92e6 100644 --- a/cli/src/ui/utils/terminalCapabilities.ts +++ b/cli/src/ui/utils/terminalCapabilities.ts @@ -9,7 +9,6 @@ */ let kittyDetected = false let kittySupported = false -const kittyEnabled = false export async function detectKittyProtocolSupport(): Promise { if (kittyDetected) { diff --git a/cli/src/utils/__tests__/git.test.ts b/cli/src/utils/__tests__/git.test.ts index 479c4803f1b..415f79ff43f 100644 --- a/cli/src/utils/__tests__/git.test.ts +++ b/cli/src/utils/__tests__/git.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { getGitInfo, getGitBranch, branchExists, generateBranchName, isGitWorktree } from "../git.js" -import simpleGit from "simple-git" +import simpleGit, { SimpleGit } from "simple-git" // Mock simple-git vi.mock("simple-git") @@ -25,10 +25,10 @@ describe("git utilities", () => { }) it("should return default info for non-git directory", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(false), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitInfo("/some/path") expect(result).toEqual({ @@ -39,12 +39,12 @@ describe("git utilities", () => { }) it("should return git info for clean repository", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue("main\n"), status: vi.fn().mockResolvedValue({ files: [] }), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitInfo("/git/repo") expect(result).toEqual({ @@ -55,14 +55,14 @@ describe("git utilities", () => { }) it("should return git info for dirty repository", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue("feature-branch\n"), status: vi.fn().mockResolvedValue({ files: [{ path: "file.txt", working_dir: "M" }], }), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitInfo("/git/repo") expect(result).toEqual({ @@ -73,10 +73,10 @@ describe("git utilities", () => { }) it("should handle errors gracefully", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitInfo("/git/repo") expect(result).toEqual({ @@ -94,31 +94,31 @@ describe("git utilities", () => { }) it("should return null for non-git directory", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(false), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitBranch("/some/path") expect(result).toBeNull() }) it("should return branch name", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue("develop\n"), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitBranch("/git/repo") expect(result).toBe("develop") }) it("should handle errors gracefully", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await getGitBranch("/git/repo") expect(result).toBeNull() @@ -137,59 +137,59 @@ describe("git utilities", () => { }) it("should return false for non-git directory", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(false), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await branchExists("/some/path", "main") expect(result).toBe(false) }) it("should return true when local branch exists", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), branch: vi.fn().mockResolvedValue({ all: ["main", "develop", "feature-branch"], }), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await branchExists("/git/repo", "feature-branch") expect(result).toBe(true) }) it("should return true when remote branch exists", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), branch: vi.fn().mockResolvedValue({ all: ["main", "remotes/origin/feature-branch"], }), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await branchExists("/git/repo", "feature-branch") expect(result).toBe(true) }) it("should return false when branch does not exist", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), branch: vi.fn().mockResolvedValue({ all: ["main", "develop"], }), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await branchExists("/git/repo", "nonexistent") expect(result).toBe(false) }) it("should handle errors gracefully", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await branchExists("/git/repo", "main") expect(result).toBe(false) @@ -265,42 +265,42 @@ describe("git utilities", () => { }) it("should return false for non-git directory", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(false), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await isGitWorktree("/some/path") expect(result).toBe(false) }) it("should return false for regular git repository", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue(".git\n"), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await isGitWorktree("/git/repo") expect(result).toBe(false) }) it("should return true for git worktree", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockResolvedValue(true), revparse: vi.fn().mockResolvedValue("/path/to/.git/worktrees/feature-branch\n"), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await isGitWorktree("/git/worktree") expect(result).toBe(true) }) it("should handle errors gracefully", async () => { - const mockGit = { + const mockGit: Partial = { checkIsRepo: vi.fn().mockRejectedValue(new Error("Git error")), } - vi.mocked(simpleGit).mockReturnValue(mockGit as any) + vi.mocked(simpleGit).mockReturnValue(mockGit as SimpleGit) const result = await isGitWorktree("/git/repo") expect(result).toBe(false) diff --git a/cli/src/utils/__tests__/providers.test.ts b/cli/src/utils/__tests__/providers.test.ts index dd44c68050d..e6818445056 100644 --- a/cli/src/utils/__tests__/providers.test.ts +++ b/cli/src/utils/__tests__/providers.test.ts @@ -104,7 +104,7 @@ describe("getSelectedModelId", () => { it("should handle mixed case provider names", () => { const apiConfig = { kilocodeModel: "test-model" } - const result = getSelectedModelId("KiloCode" as any, apiConfig) + const result = getSelectedModelId("KiloCode", apiConfig) expect(result).toBe("default") // Will be treated as unknown provider }) }) diff --git a/cli/src/utils/context.ts b/cli/src/utils/context.ts index 6280d702944..5981e97ab4b 100644 --- a/cli/src/utils/context.ts +++ b/cli/src/utils/context.ts @@ -4,6 +4,7 @@ import type { ExtensionChatMessage, ProviderSettings } from "../types/messages.js" import type { RouterModels } from "../constants/providers/models.js" +import type { ProviderConfig } from "../config/types.js" import { getCurrentModelId, getModelsByProvider } from "../constants/providers/models.js" import { logs } from "../services/logs.js" @@ -39,9 +40,10 @@ function getContextWindowFromModel(apiConfig: ProviderSettings | null, routerMod // Get current model ID const currentModelId = getCurrentModelId({ providerConfig: { - provider: apiConfig.apiProvider, + id: "default", + provider: apiConfig.apiProvider || "", ...apiConfig, - } as any, + } as ProviderConfig, routerModels, kilocodeDefaultModel: apiConfig.kilocodeModel || "", }) @@ -92,7 +94,7 @@ function getContextTokensFromMessages(messages: ExtensionChatMessage[]): number } } else if (message.type === "say" && message.say === "condense_context") { // Check if message has contextCondense metadata - const contextCondense = (message as any).contextCondense + const contextCondense = message.metadata as { newContextTokens?: number } | undefined if (contextCondense && typeof contextCondense.newContextTokens === "number") { return contextCondense.newContextTokens } @@ -177,7 +179,7 @@ export function calculateContextUsage( } // Get maxTokens setting if available - const maxTokens = apiConfig?.apiModelMaxTokens + const maxTokens = typeof apiConfig?.modelMaxTokens === "number" ? apiConfig.modelMaxTokens : undefined // Calculate token distribution const distribution = calculateTokenDistribution(contextWindow, contextTokens, maxTokens) diff --git a/cli/src/utils/notifications.ts b/cli/src/utils/notifications.ts index d56ba650cd0..6db7ccef7db 100644 --- a/cli/src/utils/notifications.ts +++ b/cli/src/utils/notifications.ts @@ -28,7 +28,7 @@ export async function fetchKilocodeNotifications({ return [] } - if (!kilocodeToken) { + if (!kilocodeToken || typeof kilocodeToken !== "string") { logs.debug("No kilocode token found, skipping notification fetch", "fetchKilocodeNotifications") return [] } diff --git a/cli/src/utils/providers.ts b/cli/src/utils/providers.ts index ea031952c68..c79b5cb36ec 100644 --- a/cli/src/utils/providers.ts +++ b/cli/src/utils/providers.ts @@ -11,7 +11,7 @@ import { getModelFieldForProvider } from "../constants/providers/models.js" */ export const getSelectedModelId = ( provider: ProviderName | string, - apiConfiguration: Record | undefined, + apiConfiguration: Record | undefined, ): string => { if (!apiConfiguration || !provider || provider === "unknown") { return "unknown" @@ -27,7 +27,7 @@ export const getSelectedModelId = ( // Special handling for vscode-lm provider if (provider === "vscode-lm") { - const selector = apiConfiguration.vsCodeLmModelSelector + const selector = apiConfiguration.vsCodeLmModelSelector as { vendor?: string; family?: string } | undefined if (selector && selector.vendor && selector.family) { return `${selector.vendor}/${selector.family}` } @@ -38,5 +38,5 @@ export const getSelectedModelId = ( const modelId = apiConfiguration[modelField] // Return the model ID or "unknown" if not set - return modelId || "unknown" + return typeof modelId === "string" ? modelId : "unknown" } diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 0d047b763f2..a795b26efa3 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -23,8 +23,13 @@ "verbatimModuleSyntax": false, "jsx": "react-jsx", "jsxImportSource": "react", - "types": ["node"] + "types": ["node"], + "baseUrl": ".", + "paths": { + "@roo/*": ["../src/shared/*"] + }, + "skipLibCheck": true }, - "include": ["src/**/*"], + "include": ["src"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1f29438c4e..c9bf6ef19c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -901,6 +901,9 @@ importers: specifier: ^3.25.61 version: 3.25.76 devDependencies: + '@eslint/js': + specifier: ^9.22.0 + version: 9.28.0 '@roo-code/config-eslint': specifier: workspace:^ version: link:../packages/config-eslint @@ -928,6 +931,12 @@ importers: del-cli: specifier: ^5.1.0 version: 5.1.0 + eslint-config-prettier: + specifier: ^10.1.1 + version: 10.1.8(eslint@9.28.0(jiti@2.6.1)) + eslint-plugin-turbo: + specifier: ^2.4.4 + version: 2.5.6(eslint@9.28.0(jiti@2.6.1))(turbo@2.6.0) ink-testing-library: specifier: ^4.0.0 version: 4.0.0(@types/react@18.3.23) @@ -946,6 +955,9 @@ importers: typescript: specifier: ^5.4.5 version: 5.6.3 + typescript-eslint: + specifier: ^8.26.0 + version: 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) vitest: specifier: ^3.2.3 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.0) @@ -26787,6 +26799,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/type-utils': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.32.1 + eslint: 9.28.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.1 @@ -26799,6 +26828,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.32.1 + debug: 4.4.1(supports-color@8.1.1) + eslint: 9.28.0(jiti@2.6.1) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.32.1': dependencies: '@typescript-eslint/types': 8.32.1 @@ -26815,8 +26856,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + debug: 4.4.3 + eslint: 9.28.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.32.1': {} + '@typescript-eslint/typescript-estree@8.32.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/visitor-keys': 8.32.1 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.32.1(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.32.1 @@ -26842,6 +26908,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.6.3) + eslint: 9.28.0(jiti@2.6.1) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.32.1': dependencies: '@typescript-eslint/types': 8.32.1 @@ -30280,6 +30357,10 @@ snapshots: dependencies: eslint: 9.27.0(jiti@2.6.1) + eslint-config-prettier@10.1.8(eslint@9.28.0(jiti@2.6.1)): + dependencies: + eslint: 9.28.0(jiti@2.6.1) + eslint-plugin-only-warn@1.1.0: {} eslint-plugin-react-hooks@5.2.0(eslint@9.27.0(jiti@2.6.1)): @@ -30314,6 +30395,12 @@ snapshots: eslint: 9.27.0(jiti@2.6.1) turbo: 2.6.0 + eslint-plugin-turbo@2.5.6(eslint@9.28.0(jiti@2.6.1))(turbo@2.6.0): + dependencies: + dotenv: 16.0.3 + eslint: 9.28.0(jiti@2.6.1) + turbo: 2.6.0 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -39359,6 +39446,10 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 + ts-api-utils@2.1.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -39610,6 +39701,16 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/parser': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + '@typescript-eslint/utils': 8.32.1(eslint@9.28.0(jiti@2.6.1))(typescript@5.6.3) + eslint: 9.28.0(jiti@2.6.1) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + typescript@4.9.5: {} typescript@5.6.3: {} diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 740c0751769..1a10dbb66f1 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -273,7 +273,9 @@ async function loadAgentRulesFile(cwd: string): Promise { await resolveSymLink(agentPath, fileInfo, 0) // Extract the resolved path from fileInfo - if (fileInfo.length > 0) { + // kilocode_change start - add null check for fileInfo[0] + if (fileInfo.length > 0 && fileInfo[0]) { + // kilocode_change end resolvedPath = fileInfo[0].resolvedPath } } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 0299280fd09..4d61c4fac97 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -534,6 +534,7 @@ export type WebViewMessagePayload = | SeeNewChangesPayload | TasksByIdRequestPayload | TaskHistoryRequestPayload + | RequestCheckpointRestoreApprovalPayload // kilocode_change end | CheckpointDiffPayload | CheckpointRestorePayload diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 9586731f189..f04bd7af2db 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -141,7 +141,9 @@ export function getModeSelection(mode: string, promptComponent?: PromptComponent } // Otherwise, use built-in mode as base and merge with promptComponent - const baseMode = builtInMode || modes[0] // fallback to default mode + // kilocode_change start - ensure baseMode is never undefined with explicit assertion + const baseMode = (builtInMode || modes[0])! + // kilocode_change end return { roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition || "", @@ -310,7 +312,9 @@ export async function getFullModeDetails( }, ): Promise { // First get the base mode config from custom modes or built-in modes - const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0] + // kilocode_change start - ensure baseMode is never undefined with explicit assertion + const baseMode = (getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0])! + // kilocode_change end // Check for any prompt component overrides const promptComponent = customModePrompts?.[modeSlug] @@ -323,12 +327,18 @@ export async function getFullModeDetails( // If we have cwd, load and combine all custom instructions let fullCustomInstructions = baseCustomInstructions if (options?.cwd) { + // kilocode_change start - only pass language if defined to satisfy exactOptionalPropertyTypes + const customInstructionsOptions: Parameters[4] = {} + if (options.language !== undefined) { + customInstructionsOptions.language = options.language + } + // kilocode_change end fullCustomInstructions = await addCustomInstructions( baseCustomInstructions, options.globalCustomInstructions || "", options.cwd, modeSlug, - { language: options.language }, + customInstructionsOptions, // kilocode_change ) }