diff --git a/packages/config-eslint/base.js b/packages/config-eslint/base.js index 6a15f09c03..6f579e2f97 100644 --- a/packages/config-eslint/base.js +++ b/packages/config-eslint/base.js @@ -2,8 +2,6 @@ import js from "@eslint/js" import eslintConfigPrettier from "eslint-config-prettier" import turboPlugin from "eslint-plugin-turbo" import tseslint from "typescript-eslint" -import onlyWarn from "eslint-plugin-only-warn" - /** * A shared ESLint configuration for the repository. * @@ -21,11 +19,6 @@ export const config = [ "turbo/no-undeclared-env-vars": "off", }, }, - { - plugins: { - onlyWarn, - }, - }, { ignores: ["dist/**"], }, diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index 8c10849f33..8c01525543 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -12,7 +12,6 @@ "@next/eslint-plugin-next": "15.5.20", "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", - "eslint-plugin-only-warn": "1.2.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-turbo": "2.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7513d35d27..ea26770f32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,9 +262,6 @@ importers: eslint-config-prettier: specifier: 10.1.8 version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-only-warn: - specifier: 1.2.1 - version: 1.2.1 eslint-plugin-react: specifier: 7.37.5 version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -4599,9 +4596,6 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-only-warn@1.2.1: - resolution: {integrity: sha512-j37hwfaQDEOfkZ1Dpvu/HnWLavlzQxQxfbrU/9Jb4R9qzrE1eTYuRJyrxq7LzLRI8miG5FOV6veoUVhx7AI84w==} - eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -12424,8 +12418,6 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-only-warn@1.2.1: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) diff --git a/src/__tests__/helpers/provider-stub.ts b/src/__tests__/helpers/provider-stub.ts index c91016d93a..59dde33933 100644 --- a/src/__tests__/helpers/provider-stub.ts +++ b/src/__tests__/helpers/provider-stub.ts @@ -1,21 +1,55 @@ import { ClineProvider } from "../../core/webview/ClineProvider" +import { TaskRegistry } from "../../core/task/TaskRegistry" +import { type Task } from "../../core/task/Task" + +type ProviderStubFields = { + delegationTransitionLocks?: Map> + cancelledDelegationChildIds?: Set + log?: ReturnType + taskHistoryStore?: { get: (id: string) => unknown } + taskRegistry?: TaskRegistry + clineStack?: Task[] + tasks?: Task[] + runDelegationTransition?: unknown + removeClineFromStack?: unknown + evictCurrentTask?: unknown +} + +type PrivateProviderMethods = { + runDelegationTransition: (this: unknown, ...args: unknown[]) => unknown + removeClineFromStack: (this: unknown, ...args: unknown[]) => unknown + evictCurrentTask: (this: unknown, ...args: unknown[]) => unknown +} /** * Augments a plain stub object with the instance fields and bound methods that * ClineProvider methods read from `this` (runDelegationTransition, * delegationTransitionLocks, cancelledDelegationChildIds, cancellingDelegationChildIds), - * so tests can call private methods via `(ClineProvider.prototype as any).method.call(stub, …)` + * so tests can call private ClineProvider methods against a plain object * without instantiating a real ClineProvider. + * + * Pass `tasks` (array of Task mocks) to pre-seed the registry in stack order. + * The legacy `clineStack` key is accepted and converted automatically. */ -export function makeProviderStub(stub: T): T { - const s = stub as any - const proto = ClineProvider.prototype as any +export function makeProviderStub(stub: T): ClineProvider { + const s = stub as T & ProviderStubFields + const proto = ClineProvider.prototype as unknown as PrivateProviderMethods s.delegationTransitionLocks ??= new Map() s.cancelledDelegationChildIds ??= new Set() s.log ??= vi.fn() s.taskHistoryStore ??= { get: () => undefined } - s.runDelegationTransition = proto.runDelegationTransition.bind(s) + + // Convert legacy clineStack array into a TaskRegistry + if (!s.taskRegistry) { + const registry = new TaskRegistry() + const seed: Task[] = s.clineStack ?? s.tasks ?? [] + for (const t of seed) registry.push(t) + s.taskRegistry = registry + } + delete s.clineStack + + s.runDelegationTransition ??= proto.runDelegationTransition.bind(s) s.removeClineFromStack ??= proto.removeClineFromStack.bind(s) s.evictCurrentTask ??= proto.evictCurrentTask.bind(s) - return s + return s as unknown as ClineProvider } diff --git a/src/__tests__/removeClineFromStack-delegation.spec.ts b/src/__tests__/removeClineFromStack-delegation.spec.ts index a91900c38a..e211a8f4ab 100644 --- a/src/__tests__/removeClineFromStack-delegation.spec.ts +++ b/src/__tests__/removeClineFromStack-delegation.spec.ts @@ -1,10 +1,29 @@ // npx vitest run __tests__/removeClineFromStack-delegation.spec.ts -import { describe, it, expect, vi } from "vitest" +import { describe, it, expect, vi, type MockedFunction } from "vitest" import { ClineProvider } from "../core/webview/ClineProvider" +import { TaskRegistry } from "../core/task/TaskRegistry" +import { type Task } from "../core/task/Task" import { makeProviderStub } from "./helpers/provider-stub" -// After the refactor: removeClineFromStack() is pure lifecycle — it pops, aborts, and +type MockTask = Pick & + Partial> & { + emit: ReturnType + abortTask: ReturnType + } + +type PrivateClineProviderMethods = { + removeClineFromStack: (this: ClineProvider) => ReturnType + markDelegatedChildInterrupted: ( + this: ClineProvider, + ...args: Parameters + ) => ReturnType + evictCurrentTask: (this: ClineProvider) => ReturnType +} + +const privateClineProvider = ClineProvider.prototype as unknown as PrivateClineProviderMethods + +// After the refactor: removeClineFromStack() is pure lifecycle — it removes the focused task, aborts, and // cleans up listeners. It does NOT mutate delegation metadata. All delegated→active // transitions are owned by reopenParentFromDelegation() (normal child completion) or // markDelegatedChildInterrupted() (live eviction via navigation / new-task / clear). @@ -12,7 +31,7 @@ import { makeProviderStub } from "./helpers/provider-stub" function buildMockProvider(opts: { childTaskId: string parentTaskId?: string - parentHistoryItem?: Record + parentHistoryItem?: Record childStatus?: string }) { const childTask = { @@ -31,13 +50,13 @@ function buildMockProvider(opts: { throw new Error("Task not found") }) - const taskHistoryStoreData: Record = {} + const taskHistoryStoreData: Record = {} if (opts.childStatus) { taskHistoryStoreData[opts.childTaskId] = { status: opts.childStatus } } const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -49,17 +68,47 @@ function buildMockProvider(opts: { } describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation side effects", () => { - it("pops the task, aborts it, and clears listeners", async () => { + it("removes the focused task, aborts it, and clears listeners", async () => { const { provider, childTask } = buildMockProvider({ childTaskId: "child-1" }) - expect(provider.clineStack).toHaveLength(1) + expect(provider["taskRegistry"].length).toBe(1) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(childTask.abortTask).toHaveBeenCalledWith(true) expect(childTask.emit).toHaveBeenCalledWith(expect.stringContaining("taskUnfocused")) }) + it("removes the focused task even when it is not the top stack entry", async () => { + const focusedTask = { + taskId: "focused-1", + instanceId: "focused-inst", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + const topTask = { + taskId: "top-1", + instanceId: "top-inst", + emit: vi.fn(), + abortTask: vi.fn().mockResolvedValue(undefined), + } + const provider = makeProviderStub({ + tasks: [focusedTask, topTask] as unknown as Task[], + taskEventListeners: new Map(), + log: vi.fn(), + getTaskWithId: vi.fn(), + updateTaskHistory: vi.fn(), + }) + provider["taskRegistry"].setCurrent("focused-1") + + await privateClineProvider.removeClineFromStack.call(provider) + + expect(provider["taskRegistry"].taskIds).toEqual(["top-1"]) + expect(provider["taskRegistry"].current).toBe(topTask) + expect(focusedTask.abortTask).toHaveBeenCalledWith(true) + expect(topTask.abortTask).not.toHaveBeenCalled() + }) + it("does NOT mutate parent metadata when a delegated child is popped (repair removed)", async () => { const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({ childTaskId: "child-1", @@ -72,9 +121,9 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation }, }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) // Navigation/disposal must never silently flip the parent to active expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() @@ -92,9 +141,9 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation childStatus: "interrupted", }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) @@ -104,26 +153,26 @@ describe("ClineProvider.removeClineFromStack() — pure lifecycle, no delegation childTaskId: "standalone-1", }) - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(getTaskWithId).not.toHaveBeenCalled() expect(updateTaskHistory).not.toHaveBeenCalled() }) it("handles empty stack gracefully", async () => { const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId: vi.fn(), updateTaskHistory: vi.fn(), }) - await expect((ClineProvider.prototype as any).removeClineFromStack.call(provider)).resolves.not.toThrow() + await expect(privateClineProvider.removeClineFromStack.call(provider)).resolves.not.toThrow() - expect((provider as any).getTaskWithId).not.toHaveBeenCalled() - expect((provider as any).updateTaskHistory).not.toHaveBeenCalled() + expect(provider["getTaskWithId"]).not.toHaveBeenCalled() + expect(provider["updateTaskHistory"]).not.toHaveBeenCalled() }) }) @@ -159,7 +208,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const postMessageToWebview = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -170,7 +219,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -203,7 +252,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const updateTaskHistory = vi.fn().mockResolvedValue([]) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId: vi.fn(), @@ -213,7 +262,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -235,7 +284,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -245,7 +294,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -270,8 +319,11 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", return { historyItem: { id: childTaskId, status: "interrupted", parentTaskId } } }) + const realRunDelegation = ClineProvider.prototype["runDelegationTransition"].bind({ + delegationTransitionLocks: new Map(), + }) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -289,16 +341,14 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", return undefined }), }, + // Wrap the real runDelegationTransition to set lockAcquired before the lock fires + runDelegationTransition: async (parentId: string, fn: () => Promise) => { + lockAcquired = true + return realRunDelegation(parentId, fn) + }, }) - // Patch runDelegationTransition to set lockAcquired before calling fn - const realRunDelegation = (provider as any).runDelegationTransition.bind(provider) - ;(provider as any).runDelegationTransition = async (_parentId: string, fn: () => Promise) => { - lockAcquired = true - return realRunDelegation(_parentId, fn) - } - - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -315,7 +365,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", const getTaskWithId = vi.fn().mockRejectedValue(new Error("store unavailable")) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log, getTaskWithId, @@ -326,7 +376,7 @@ describe("ClineProvider.markDelegatedChildInterrupted() — live eviction path", }) await expect( - (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }), @@ -374,7 +424,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -393,7 +443,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation // Simulate the navigation logic from createTaskWithHistoryItem: // when the target is a delegated parent and current task is its interrupted child, // removeClineFromStack must NOT repair parent to active. - await (ClineProvider.prototype as any).removeClineFromStack.call(provider) + await privateClineProvider.removeClineFromStack.call(provider) // Parent must stay delegated — no write at all expect(updateTaskHistory).not.toHaveBeenCalledWith(expect.objectContaining({ id: parentTaskId })) @@ -428,7 +478,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation const postMessageToWebview = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), log: vi.fn(), getTaskWithId, @@ -444,7 +494,7 @@ describe("createTaskWithHistoryItem() navigation — does not mutate delegation }, }) - await (ClineProvider.prototype as any).markDelegatedChildInterrupted.call(provider, { + await privateClineProvider.markDelegatedChildInterrupted.call(provider, { childTaskId, parentTaskId, }) @@ -480,7 +530,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn().mockResolvedValue(undefined) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { get: vi.fn((id: string) => (id === childTaskId ? childHistoryItem : undefined)) }, @@ -488,9 +538,9 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) - expect(provider.clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(markDelegatedChildInterrupted).toHaveBeenCalledWith({ childTaskId, parentTaskId }) }) @@ -498,7 +548,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn() const provider = makeProviderStub({ - clineStack: [] as any[], + clineStack: [] as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => undefined), taskHistoryStore: { get: vi.fn(() => undefined) }, @@ -506,7 +556,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() }) @@ -522,7 +572,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn() const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { @@ -532,7 +582,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await (ClineProvider.prototype as any).evictCurrentTask.call(provider) + await privateClineProvider.evictCurrentTask.call(provider) expect(markDelegatedChildInterrupted).not.toHaveBeenCalled() }) @@ -552,7 +602,7 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () const markDelegatedChildInterrupted = vi.fn().mockRejectedValue(new Error("lock contention")) const provider = makeProviderStub({ - clineStack: [childTask] as any[], + clineStack: [childTask] as unknown as Task[], taskEventListeners: new Map(), getCurrentTask: vi.fn(() => childTask), taskHistoryStore: { @@ -562,14 +612,14 @@ describe("ClineProvider.evictCurrentTask() — active delegated child path", () log: vi.fn(), }) - await expect((ClineProvider.prototype as any).evictCurrentTask.call(provider)).rejects.toThrow( - "lock contention", - ) + await expect(privateClineProvider.evictCurrentTask.call(provider)).rejects.toThrow("lock contention") }) }) describe("onTaskCompleted callback — writes completed status before re-emitting", () => { - function buildCallbackProvider(taskHistoryStoreGet: (id: string) => any) { + type CallbackHistoryItem = { id: string; status?: string; [key: string]: unknown } + + function buildCallbackProvider(taskHistoryStoreGet: (id: string) => CallbackHistoryItem | undefined) { const updateTaskHistory = vi.fn().mockResolvedValue([]) const emit = vi.fn() const log = vi.fn() @@ -584,7 +634,7 @@ describe("onTaskCompleted callback — writes completed status before re-emittin listeners[event] = listeners[event] || [] listeners[event].push(fn) }), - emit: vi.fn((event: string, ...args: any[]) => { + emit: vi.fn((event: string, ...args: unknown[]) => { listeners[event]?.forEach((fn) => fn(...args)) }), } @@ -602,16 +652,16 @@ describe("onTaskCompleted callback — writes completed status before re-emittin // The real callback is set in the constructor body; we replicate the relevant portion. const onTaskCompleted = async (taskId: string) => { try { - const existing = (provider as any).taskHistoryStore.get(taskId) + const existing = provider["taskHistoryStore"].get(taskId) if (existing && existing.status !== "completed") { - await (provider as any).updateTaskHistory({ ...existing, status: "completed" }) + await provider["updateTaskHistory"]({ ...existing, status: "completed" }) } } catch (err) { - ;(provider as any).log( + provider["log"]( `[onTaskCompleted] Failed to write completed status for ${taskId}: ${err instanceof Error ? err.message : String(err)}`, ) } - ;(provider as any).emit("TaskCompleted", taskId, {}, {}) + emit("TaskCompleted", taskId, {}, {}) } return { onTaskCompleted, updateTaskHistory, emit, log } @@ -660,7 +710,7 @@ describe("onTaskCompleted callback — writes completed status before re-emittin const { onTaskCompleted, updateTaskHistory, log } = buildCallbackProvider((id) => id === "task-1" ? existingItem : undefined, ) - ;(updateTaskHistory as any).mockRejectedValue(new Error("disk full")) + vi.mocked(updateTaskHistory).mockRejectedValue(new Error("disk full")) await expect(onTaskCompleted("task-1")).resolves.not.toThrow() diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 29aab5d15d..0cb9425680 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -1,19 +1,44 @@ // npx vitest run __tests__/single-open-invariant.spec.ts import { describe, it, expect, vi, beforeEach } from "vitest" +import { type OutputChannel } from "vscode" import { ClineProvider } from "../core/webview/ClineProvider" +import { TaskRegistry } from "../core/task/TaskRegistry" +import { type Task } from "../core/task/Task" import { API } from "../extension/api" import * as ProfileValidatorMod from "../shared/ProfileValidator" +type PrivateClineProviderMethods = { + createTask: ( + this: unknown, + text?: string, + images?: string[], + parentTask?: Task, + options?: Parameters[3], + ) => ReturnType + createTaskWithHistoryItem: ( + this: unknown, + ...args: Parameters + ) => ReturnType + evictCurrentTask: (this: unknown) => ReturnType +} + +const privateClineProvider = ClineProvider.prototype as unknown as PrivateClineProviderMethods + // Mock Task class used by ClineProvider to avoid heavy startup vi.mock("../core/task/Task", () => { class TaskStub { public taskId: string public instanceId = "inst" - public parentTask?: any - public apiConfiguration: any - public rootTask?: any - constructor(opts: any) { + public parentTask?: unknown + public apiConfiguration: unknown + public rootTask?: unknown + constructor(opts: { + historyItem?: { id: string } + parentTask?: unknown + apiConfiguration?: unknown + onCreated?: (t: TaskStub) => void + }) { this.taskId = opts.historyItem?.id ?? `task-${Math.random().toString(36).slice(2, 8)}` this.parentTask = opts.parentTask this.apiConfiguration = opts.apiConfiguration ?? { apiProvider: "anthropic" } @@ -39,15 +64,16 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) - const existingTask = { taskId: "existing-1" } + const existingTask = { taskId: "existing-1", abort: false, abandoned: false } + const registry = new TaskRegistry() + registry.push(existingTask as unknown as Task) const provider = { - // Simulate an existing task present in stack - clineStack: [existingTask], + taskRegistry: registry, getCurrentTask: vi.fn(() => existingTask), taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ @@ -74,7 +100,7 @@ describe("Single-open-task invariant", () => { }, } as unknown as ClineProvider - await (ClineProvider.prototype as any).createTask.call(provider, "New task") + await privateClineProvider.createTask.call(provider, "New task") expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -85,10 +111,12 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const addClineToStack = vi.fn().mockResolvedValue(undefined) - const parentTask = { taskId: "parent-1" } + const parentTask = { taskId: "parent-1", abort: false, abandoned: false } + const registry2 = new TaskRegistry() + registry2.push(parentTask as unknown as Task) const provider = { - clineStack: [parentTask], + taskRegistry: registry2, setValues: vi.fn(), getState: vi.fn().mockResolvedValue({ apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 }, @@ -114,7 +142,7 @@ describe("Single-open-task invariant", () => { }, } as unknown as ClineProvider - await (ClineProvider.prototype as any).createTask.call(provider, "Subtask", undefined, parentTask as any) + await privateClineProvider.createTask.call(provider, "Subtask", undefined, parentTask as unknown as Task) expect(removeClineFromStack).not.toHaveBeenCalled() expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -130,7 +158,7 @@ describe("Single-open-task invariant", () => { taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, removeClineFromStack, addClineToStack, @@ -174,7 +202,7 @@ describe("Single-open-task invariant", () => { workspace: "/tmp", } - const task = await (ClineProvider.prototype as any).createTaskWithHistoryItem.call(provider, historyItem) + const task = await privateClineProvider.createTaskWithHistoryItem.call(provider, historyItem) expect(task).toBeTruthy() expect(removeClineFromStack).toHaveBeenCalledTimes(1) expect(addClineToStack).toHaveBeenCalledTimes(1) @@ -184,12 +212,12 @@ describe("Single-open-task invariant", () => { const removeClineFromStack = vi.fn().mockResolvedValue(undefined) const createTask = vi.fn().mockResolvedValue({ taskId: "ipc-1" }) const provider = { - context: {} as any, + context: {} as unknown, getCurrentTask: vi.fn(() => undefined), taskHistoryStore: { get: vi.fn(() => undefined) }, markDelegatedChildInterrupted: vi.fn().mockResolvedValue(undefined), get evictCurrentTask() { - return (ClineProvider.prototype as any).evictCurrentTask.bind(this) + return privateClineProvider.evictCurrentTask.bind(this) }, removeClineFromStack, postStateToWebview: vi.fn(), @@ -197,7 +225,7 @@ describe("Single-open-task invariant", () => { createTask, getValues: vi.fn(() => ({})), providerSettingsManager: { saveConfig: vi.fn() }, - on: vi.fn((ev: any, cb: any) => { + on: vi.fn((ev: unknown, cb: unknown) => { if (ev === "taskCreated") { // no-op for this test } @@ -205,7 +233,7 @@ describe("Single-open-task invariant", () => { }), } as unknown as ClineProvider - const output = { appendLine: vi.fn() } as any + const output = { appendLine: vi.fn() } as unknown as OutputChannel const api = new API(output, provider, undefined, false) const taskId = await api.startNewTask({ diff --git a/src/core/task/TaskRegistry.ts b/src/core/task/TaskRegistry.ts new file mode 100644 index 0000000000..728870683d --- /dev/null +++ b/src/core/task/TaskRegistry.ts @@ -0,0 +1,119 @@ +import { type Task } from "./Task" + +/** + * Expand-Contract adapter replacing `clineStack: Task[]` in ClineProvider. + * + * Phase A+B: adapter is live, all 23 call sites migrated, concurrent access + * methods available. Maintains a LIFO stack of task IDs alongside a Map for + * O(1) lookup. Invariant: tasks.size === stack.length at all times. + * + * Phase C (future): remove internal stack once all callers use map-based access. + */ +export class TaskRegistry { + private tasks = new Map() + private stack: string[] = [] + private _currentTaskId: string | undefined + + /** + * Add a task and focus it. If the task ID already exists, remove the + * existing entry first so this behaves as "move to top" instead of creating + * duplicate stack entries. + */ + push(task: Task): void { + if (this.tasks.has(task.taskId)) { + this.remove(task.taskId) + } + this.tasks.set(task.taskId, task) + this.stack.push(task.taskId) + this._currentTaskId = task.taskId + } + + pop(): Task | undefined { + const id = this.stack.pop() + if (id === undefined) return undefined + const task = this.tasks.get(id) + this.tasks.delete(id) + if (this._currentTaskId === id) { + this._currentTaskId = this.stack[this.stack.length - 1] + } + return task + } + + get current(): Task | undefined { + return this._currentTaskId !== undefined ? this.tasks.get(this._currentTaskId) : undefined + } + + /** + * Switch UI focus to a different task without mutating the stack order. + * Throws if the taskId is not in the registry. + */ + setCurrent(taskId: string): void { + if (!this.tasks.has(taskId)) { + throw new Error(`[TaskRegistry] setCurrent: unknown taskId ${taskId}`) + } + this._currentTaskId = taskId + } + + getById(id: string): Task | undefined { + return this.tasks.get(id) + } + + getAll(): Task[] { + return this.stack.map((id) => this.tasks.get(id)!) + } + + /** All tasks that are not aborted or abandoned. */ + getRunning(): Task[] { + return this.getAll().filter((t) => !t.abort && !t.abandoned) + } + + /** True only if the task is in the registry and is not aborted or abandoned. */ + hasRunning(taskId: string): boolean { + const t = this.tasks.get(taskId) + return t !== undefined && !t.abort && !t.abandoned + } + + /** Remove a task by ID regardless of its stack position. */ + remove(taskId: string): Task | undefined { + const task = this.tasks.get(taskId) + if (task === undefined) return undefined + this.tasks.delete(taskId) + const idx = this.stack.indexOf(taskId) + if (idx !== -1) this.stack.splice(idx, 1) + if (this._currentTaskId === taskId) { + this._currentTaskId = this.stack[this.stack.length - 1] + } + return task + } + + /** + * Replace a task in-place: same stack index, same current pointer. + * Used by rehydration so focus and stack order are both preserved. + * Throws if taskId is not in the registry. + */ + replace(taskId: string, replacement: Task): Task { + const idx = this.stack.indexOf(taskId) + if (idx === -1) { + throw new Error(`[TaskRegistry] replace: unknown taskId ${taskId}`) + } + if (replacement.taskId !== taskId && this.tasks.has(replacement.taskId)) { + throw new Error(`[TaskRegistry] replace: duplicate taskId ${replacement.taskId}`) + } + const old = this.tasks.get(taskId)! + this.tasks.delete(taskId) + this.tasks.set(replacement.taskId, replacement) + this.stack[idx] = replacement.taskId + if (this._currentTaskId === taskId) { + this._currentTaskId = replacement.taskId + } + return old + } + + get length(): number { + return this.stack.length + } + + get taskIds(): string[] { + return [...this.stack] + } +} diff --git a/src/core/task/__tests__/TaskRegistry.spec.ts b/src/core/task/__tests__/TaskRegistry.spec.ts new file mode 100644 index 0000000000..4493d83277 --- /dev/null +++ b/src/core/task/__tests__/TaskRegistry.spec.ts @@ -0,0 +1,301 @@ +import { TaskRegistry } from "../TaskRegistry" +import { type Task } from "../Task" + +function makeTask(taskId: string, opts: { abort?: boolean; abandoned?: boolean } = {}): Task { + return { taskId, abort: opts.abort ?? false, abandoned: opts.abandoned ?? false } as unknown as Task +} + +function assertInvariant(registry: TaskRegistry) { + const ids = registry.taskIds + expect(ids.length).toBe(registry.length) + for (const id of ids) { + expect(registry.getById(id)).toBeDefined() + } + if (registry.current !== undefined) { + expect(registry.getById(registry.current.taskId)).toBe(registry.current) + } +} + +describe("TaskRegistry", () => { + describe("push / pop", () => { + it("push adds to registry and makes task current", () => { + const r = new TaskRegistry() + const t = makeTask("a") + r.push(t) + expect(r.length).toBe(1) + expect(r.current).toBe(t) + expect(r.getById("a")).toBe(t) + assertInvariant(r) + }) + + it("push of second task makes it current", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(r.current).toBe(t2) + expect(r.length).toBe(2) + assertInvariant(r) + }) + + it("push replaces duplicate task ids", () => { + const r = new TaskRegistry() + const first = makeTask("a") + const second = makeTask("a") + r.push(first) + r.push(second) + expect(r.taskIds).toEqual(["a"]) + expect(r.length).toBe(1) + expect(r.current).toBe(second) + expect(r.getById("a")).toBe(second) + assertInvariant(r) + }) + + it("pop removes top task and restores previous current", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + const popped = r.pop() + expect(popped).toBe(t2) + expect(r.length).toBe(1) + expect(r.current).toBe(t1) + expect(r.getById("b")).toBeUndefined() + assertInvariant(r) + }) + + it("pop on empty registry returns undefined", () => { + const r = new TaskRegistry() + expect(r.pop()).toBeUndefined() + expect(r.length).toBe(0) + expect(r.current).toBeUndefined() + }) + + it("pop of last task leaves current undefined", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.pop() + expect(r.current).toBeUndefined() + expect(r.length).toBe(0) + assertInvariant(r) + }) + + it("pop of non-current top does not change focus", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + r.push(t1) + r.push(t2) + r.push(t3) + r.setCurrent("a") + const popped = r.pop() + expect(popped).toBe(t3) + expect(r.current).toBe(t1) + expect(r.taskIds).toEqual(["a", "b"]) + assertInvariant(r) + }) + }) + + describe("getById", () => { + it("returns correct task by id", () => { + const r = new TaskRegistry() + const t = makeTask("x") + r.push(t) + expect(r.getById("x")).toBe(t) + }) + + it("returns undefined for unknown id", () => { + const r = new TaskRegistry() + expect(r.getById("nope")).toBeUndefined() + }) + }) + + describe("remove", () => { + it("removes task by id regardless of position", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + r.push(t1) + r.push(t2) + r.push(t3) + const removed = r.remove("b") + expect(removed).toBe(t2) + expect(r.length).toBe(2) + expect(r.getById("b")).toBeUndefined() + expect(r.taskIds).toEqual(["a", "c"]) + assertInvariant(r) + }) + + it("remove of current task updates current to new top", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + r.remove("b") + expect(r.current).toBe(t1) + assertInvariant(r) + }) + + it("returns undefined for unknown id", () => { + const r = new TaskRegistry() + expect(r.remove("nope")).toBeUndefined() + }) + }) + + describe("replace", () => { + it("preserves stack index when replacing the current task", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t3 = makeTask("c") + const t2v2 = makeTask("b-v2") + r.push(t1) + r.push(t2) + r.push(t3) + r.setCurrent("b") + r.replace("b", t2v2) + expect(r.taskIds).toEqual(["a", "b-v2", "c"]) + expect(r.current).toBe(t2v2) + expect(r.getById("b")).toBeUndefined() + expect(r.getById("b-v2")).toBe(t2v2) + assertInvariant(r) + }) + + it("preserves stack index when replacing a non-current task", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + const t1v2 = makeTask("a-v2") + r.push(t1) + r.push(t2) + r.replace("a", t1v2) + expect(r.taskIds).toEqual(["a-v2", "b"]) + expect(r.current).toBe(t2) + assertInvariant(r) + }) + + it("returns the old task", () => { + const r = new TaskRegistry() + const t = makeTask("a") + const t2 = makeTask("a-v2") + r.push(t) + expect(r.replace("a", t2)).toBe(t) + }) + + it("throws for unknown taskId", () => { + const r = new TaskRegistry() + expect(() => r.replace("nope", makeTask("x"))).toThrow(/unknown taskId/) + }) + + it("throws when replacement task id already exists elsewhere", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(() => r.replace("a", makeTask("b"))).toThrow(/duplicate taskId/) + expect(r.taskIds).toEqual(["a", "b"]) + expect(r.getById("a")).toBe(t1) + expect(r.getById("b")).toBe(t2) + assertInvariant(r) + }) + }) + + describe("setCurrent", () => { + it("changes current without mutating stack order", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + r.setCurrent("a") + expect(r.current).toBe(t1) + expect(r.taskIds).toEqual(["a", "b"]) + assertInvariant(r) + }) + + it("throws for unknown taskId", () => { + const r = new TaskRegistry() + expect(() => r.setCurrent("nope")).toThrow(/unknown taskId/) + }) + }) + + describe("getAll", () => { + it("returns tasks in stack order", () => { + const r = new TaskRegistry() + const t1 = makeTask("a") + const t2 = makeTask("b") + r.push(t1) + r.push(t2) + expect(r.getAll()).toEqual([t1, t2]) + }) + }) + + describe("getRunning / hasRunning", () => { + it("getRunning excludes aborted tasks", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.push(makeTask("b", { abort: true })) + r.push(makeTask("c")) + const running = r.getRunning() + expect(running.map((t) => t.taskId)).toEqual(["a", "c"]) + }) + + it("getRunning excludes abandoned tasks", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + r.push(makeTask("b", { abandoned: true })) + const running = r.getRunning() + expect(running.map((t) => t.taskId)).toEqual(["a"]) + }) + + it("hasRunning returns true for a live task", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + expect(r.hasRunning("a")).toBe(true) + }) + + it("hasRunning returns false for an aborted task", () => { + const r = new TaskRegistry() + r.push(makeTask("a", { abort: true })) + expect(r.hasRunning("a")).toBe(false) + }) + + it("hasRunning returns false for an unknown task", () => { + const r = new TaskRegistry() + expect(r.hasRunning("nope")).toBe(false) + }) + }) + + describe("taskIds / length", () => { + it("taskIds is a snapshot (not a live reference)", () => { + const r = new TaskRegistry() + r.push(makeTask("a")) + const ids = r.taskIds + r.push(makeTask("b")) + expect(ids).toEqual(["a"]) + }) + }) + + describe("dual-write invariant", () => { + it("holds after a sequence of mixed operations", () => { + const r = new TaskRegistry() + const tasks = (["a", "b", "c", "d"] as const).map((id) => makeTask(id)) + for (const t of tasks) { + r.push(t) + assertInvariant(r) + } + r.remove("b") + assertInvariant(r) + r.pop() + assertInvariant(r) + r.setCurrent("a") + assertInvariant(r) + }) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fcfd916d8e..643c4f5a22 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -52,6 +52,7 @@ import { isRetiredProvider, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" +import { TaskRegistry } from "../task/TaskRegistry" import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts" import { TelemetryService } from "@roo-code/telemetry" import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" @@ -164,7 +165,7 @@ export class ClineProvider private disposables: vscode.Disposable[] = [] private webviewDisposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel - private clineStack: Task[] = [] + private taskRegistry = new TaskRegistry() private delegationTransitionLocks?: Map> private cancelledDelegationChildIds = new Set() private codeIndexStatusSubscription?: vscode.Disposable @@ -454,14 +455,14 @@ export class ClineProvider this.log("Cloud profile synchronization is disabled in compatibility mode") } - // Adds a new Task instance to clineStack, marking the start of a new task. + // Adds a new Task instance to the registry, marking the start of a new task. // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the // previous task. async addClineToStack(task: Task) { // Add this cline instance into the stack that represents the order of // all the called tasks. - this.clineStack.push(task) + this.taskRegistry.push(task) task.emit(RooCodeEventName.TaskFocused) // Perform special setup provider specific tasks. @@ -496,12 +497,15 @@ export class ClineProvider // Removes and destroys the top Cline instance (the current finished task), // activating the previous one (resuming the parent task). async removeClineFromStack() { - if (this.clineStack.length === 0) { + if (this.taskRegistry.length === 0) { return } - // Pop the top Cline instance from the stack. - let task = this.clineStack.pop() + // Remove the focused Cline instance from the stack. + let task = this.taskRegistry.current + if (task) { + task = this.taskRegistry.remove(task.taskId) + } if (task) { task.emit(RooCodeEventName.TaskUnfocused) @@ -619,11 +623,11 @@ export class ClineProvider } getTaskStackSize(): number { - return this.clineStack.length + return this.taskRegistry.length } public getCurrentTaskStack(): string[] { - return this.clineStack.map((cline) => cline.taskId) + return this.taskRegistry.taskIds } // Pending Edit Operations Management @@ -681,10 +685,10 @@ export class ClineProvider // Clear all tasks from the stack. The first pop goes through evictCurrentTask() // so an active delegated child is marked interrupted before the extension shuts down, // rather than being left persisted as "active" across the reload. - if (this.clineStack.length > 0) { + if (this.taskRegistry.length > 0) { await this.evictCurrentTask() } - while (this.clineStack.length > 0) { + while (this.taskRegistry.length > 0) { await this.removeClineFromStack() } @@ -1186,29 +1190,29 @@ export class ClineProvider if (isRehydratingCurrentTask) { // Replace the current task in-place to avoid UI flicker - const stackIndex = this.clineStack.length - 1 + const oldTask = this.taskRegistry.current - // Properly dispose of the old task to ensure garbage collection - const oldTask = this.clineStack[stackIndex] + if (oldTask) { + // Abort the old task to stop running processes and mark as abandoned + try { + await oldTask.abortTask(true) + } catch (e) { + this.log( + `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`, + ) + } - // Abort the old task to stop running processes and mark as abandoned - try { - await oldTask.abortTask(true) - } catch (e) { - this.log( - `[createTaskWithHistoryItem] abortTask() failed for old task ${oldTask.taskId}.${oldTask.instanceId}: ${e.message}`, - ) - } + // Remove event listeners from the old task + const cleanupFunctions = this.taskEventListeners.get(oldTask) + if (cleanupFunctions) { + cleanupFunctions.forEach((cleanup) => cleanup()) + this.taskEventListeners.delete(oldTask) + } - // Remove event listeners from the old task - const cleanupFunctions = this.taskEventListeners.get(oldTask) - if (cleanupFunctions) { - cleanupFunctions.forEach((cleanup) => cleanup()) - this.taskEventListeners.delete(oldTask) + // Replace in-place: preserves stack index and current pointer + this.taskRegistry.replace(oldTask.taskId, task) } - // Replace the task in the stack - this.clineStack[stackIndex] = task task.emit(RooCodeEventName.TaskFocused) // Perform preparation tasks and set up event listeners @@ -2029,13 +2033,7 @@ export class ClineProvider /* Condenses a task's message history to use fewer tokens. */ async condenseTaskContext(taskId: string) { - let task: Task | undefined - for (let i = this.clineStack.length - 1; i >= 0; i--) { - if (this.clineStack[i].taskId === taskId) { - task = this.clineStack[i] - break - } - } + const task = this.taskRegistry.getById(taskId) if (!task) { throw new Error(`Task with id ${taskId} not found in stack`) } @@ -3027,11 +3025,7 @@ export class ClineProvider */ public getCurrentTask(): Task | undefined { - if (this.clineStack.length === 0) { - return undefined - } - - return this.clineStack[this.clineStack.length - 1] + return this.taskRegistry.current } private logWebviewHiddenDiagnostics(): void { @@ -3043,7 +3037,7 @@ export class ClineProvider `[Zoo Code] Webview hidden during active task.\n` + ` taskId: ${task.taskId}\n` + ` messageCount: ${task.clineMessages.length}\n` + - ` stackDepth: ${this.clineStack.length}\n` + + ` stackDepth: ${this.taskRegistry.length}\n` + ` timestamp: ${new Date().toISOString()}\n` + `If the panel appears gray after this, share this log with support@zoocode.dev`, ) @@ -3176,12 +3170,12 @@ export class ClineProvider task: text, images, experiments, - rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, + rootTask: this.taskRegistry.getAll()[0], parentTask, - taskNumber: this.clineStack.length + 1, + taskNumber: this.taskRegistry.length + 1, onCreated: this.taskCreationCallback, initialTodos: options.initialTodos, - // Ensure this task is present in clineStack before startTask() emits + // Ensure this task is present in the registry before startTask() emits // its initial state update, so state.currentTaskId is available ASAP. startTask: false, diffFuzzyThreshold, @@ -3349,8 +3343,8 @@ export class ClineProvider // Clear the current task without treating it as a subtask. // This is used when the user cancels a task that is not a subtask. public async clearTask(): Promise { - if (this.clineStack.length > 0) { - const task = this.clineStack[this.clineStack.length - 1] + const task = this.taskRegistry.current + if (task) { console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`) await this.removeClineFromStack() } diff --git a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts index 37aae2ddb7..3513bd3bd5 100644 --- a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts @@ -3,9 +3,28 @@ import * as vscode from "vscode" import { ClineProvider } from "../ClineProvider" import { Task } from "../../task/Task" +import { TaskRegistry } from "../../task/TaskRegistry" import { ContextProxy } from "../../config/ContextProxy" import type { ProviderSettings, HistoryItem } from "@roo-code/types" +type MockTask = Partial & + Pick & { + parentTaskId?: string + rootTask?: { taskId: string } + parentTask?: { taskId: string } + cancelCurrentRequest?: ReturnType + isStreaming?: boolean + didFinishAbortingStream?: boolean + isWaitingForFirstChunk?: boolean + } +type CreatedHistoryTask = Awaited> + +function seedRegistry(provider: ClineProvider, ...tasks: unknown[]) { + const registry = new TaskRegistry() + for (const t of tasks) registry.push(t as unknown as Task) + provider["taskRegistry"] = registry +} + // Mock dependencies vi.mock("vscode", () => { const mockDisposable = { dispose: vi.fn() } @@ -257,10 +276,10 @@ vi.mock("../../task-persistence", async (importOriginal) => { describe("ClineProvider flicker-free cancel", () => { let provider: ClineProvider - let mockContext: any - let mockOutputChannel: any - let mockTask1: any - let mockTask2: any + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockTask1: MockTask + let mockTask2: MockTask let consoleLogSpy: ReturnType let consoleErrorSpy: ReturnType @@ -301,13 +320,13 @@ describe("ClineProvider flicker-free cancel", () => { keys: vi.fn().mockReturnValue([]), }, extensionUri: { fsPath: "/test/extension" }, - } + } as unknown as vscode.ExtensionContext // Setup mock output channel mockOutputChannel = { appendLine: vi.fn(), dispose: vi.fn(), - } + } as unknown as vscode.OutputChannel // Setup mock context proxy const mockContextProxy = { @@ -320,7 +339,12 @@ describe("ClineProvider flicker-free cancel", () => { } // Create provider instance - provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy as any) + provider = new ClineProvider( + mockContext, + mockOutputChannel, + "sidebar", + mockContextProxy as unknown as ContextProxy, + ) // Mock provider methods provider.getState = vi.fn().mockResolvedValue({ @@ -330,8 +354,8 @@ describe("ClineProvider flicker-free cancel", () => { provider.postStateToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebviewWithoutTaskHistory = vi.fn().mockResolvedValue(undefined) - // Mock private method using any cast - ;(provider as any).updateGlobalState = vi.fn().mockResolvedValue(undefined) + // Mock private method used by the rehydration path. + provider["updateGlobalState"] = vi.fn().mockResolvedValue(undefined) provider.activateProviderProfile = vi.fn().mockResolvedValue(undefined) provider.performPreparationTasks = vi.fn().mockResolvedValue(undefined) provider.getTaskWithId = vi.fn().mockImplementation((id) => @@ -371,7 +395,7 @@ describe("ClineProvider flicker-free cancel", () => { // Mock Task constructor vi.mocked(Task).mockImplementation(function () { - return mockTask2 as any + return mockTask2 as unknown as Task }) }) @@ -380,13 +404,13 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should not remove current task from stack when rehydrating same taskId", async () => { - // Setup: Add a task to the stack first - ;(provider as any).clineStack = [mockTask1] + // Setup: Add a task to the registry first + seedRegistry(provider, mockTask1) // Mock event listeners for cleanup - ;(provider as any).taskEventListeners = new WeakMap() + provider["taskEventListeners"] = new WeakMap() const mockCleanupFunctions = [vi.fn(), vi.fn()] - ;(provider as any).taskEventListeners.set(mockTask1, mockCleanupFunctions) + provider["taskEventListeners"].set(mockTask1 as unknown as Task, mockCleanupFunctions) // Spy on removeClineFromStack to verify it's NOT called const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack") @@ -410,8 +434,9 @@ describe("ClineProvider flicker-free cancel", () => { expect(removeClineFromStackSpy).not.toHaveBeenCalled() // Verify the task was replaced in-place - expect((provider as any).clineStack).toHaveLength(1) - expect((provider as any).clineStack[0]).toBe(mockTask2) + const registry = provider["taskRegistry"] + expect(registry.length).toBe(1) + expect(registry.current).toBe(mockTask2) // Verify old event listeners were cleaned up expect(mockCleanupFunctions[0]).toHaveBeenCalled() @@ -422,12 +447,12 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should remove task from stack when creating different task", async () => { - // Setup: Add a task to the stack first - ;(provider as any).clineStack = [mockTask1] + // Setup: Add a task to the registry first + seedRegistry(provider, mockTask1) // Spy on removeClineFromStack to verify it IS called const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { - ;(provider as any).clineStack.pop() + provider["taskRegistry"].pop() }) // Create history item with different taskId @@ -450,12 +475,12 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should handle empty stack gracefully during rehydration attempt", async () => { - // Setup: Empty stack - ;(provider as any).clineStack = [] + // Setup: Empty registry (default) + seedRegistry(provider) // Spy on removeClineFromStack const removeClineFromStackSpy = vi.spyOn(provider, "removeClineFromStack").mockImplementation(async () => { - ;(provider as any).clineStack.pop() + provider["taskRegistry"].pop() }) // Create history item @@ -478,16 +503,18 @@ describe("ClineProvider flicker-free cancel", () => { }) it("should maintain task stack integrity during flicker-free replacement", async () => { - // Setup: Stack with multiple tasks + // Setup: Registry with parent task then current task const mockParentTask = { taskId: "parent-task", instanceId: "parent-instance", + abort: false, + abandoned: false, emit: vi.fn(), } - ;(provider as any).clineStack = [mockParentTask, mockTask1] - ;(provider as any).taskEventListeners = new WeakMap() - ;(provider as any).taskEventListeners.set(mockTask1, [vi.fn()]) + seedRegistry(provider, mockParentTask, mockTask1) + provider["taskEventListeners"] = new WeakMap() + provider["taskEventListeners"].set(mockTask1 as unknown as Task, [vi.fn()]) // Act: Rehydrate the current (top) task const historyItem: HistoryItem = { @@ -503,10 +530,50 @@ describe("ClineProvider flicker-free cancel", () => { await provider.createTaskWithHistoryItem(historyItem) - // Assert: Stack should maintain parent task and replace current task - expect((provider as any).clineStack).toHaveLength(2) - expect((provider as any).clineStack[0]).toBe(mockParentTask) - expect((provider as any).clineStack[1]).toBe(mockTask2) + // Assert: Registry should maintain parent task and replace current task + const registry = provider["taskRegistry"] + expect(registry.length).toBe(2) + expect(registry.getAll()[0]).toBe(mockParentTask) + expect(registry.getAll()[1]).toBe(mockTask2) + }) + + it("should preserve stack order when rehydrating a focused non-top task", async () => { + // Regression test for issue #1: if setCurrent() has focused a non-top task, + // rehydrating it must not move it to the top of the stack. + const mockTopTask = { + taskId: "top-task", + instanceId: "top-instance", + abort: false, + abandoned: false, + emit: vi.fn(), + } + + // Seed: [mockTask1 (focused), mockTopTask (top-of-stack)] + seedRegistry(provider, mockTask1, mockTopTask) + provider["taskRegistry"].setCurrent("task-1") + provider["taskEventListeners"] = new WeakMap() + provider["taskEventListeners"].set(mockTask1 as unknown as Task, [vi.fn()]) + + const historyItem: HistoryItem = { + id: "task-1", + number: 1, + task: "test task", + ts: Date.now(), + tokensIn: 100, + tokensOut: 200, + totalCost: 0.001, + workspace: "/test/workspace", + } + + await provider.createTaskWithHistoryItem(historyItem) + + const registry = provider["taskRegistry"] + // Stack order must be unchanged: replacement stays at index 0, top-task stays at index 1 + expect(registry.length).toBe(2) + expect(registry.getAll()[0]).toBe(mockTask2) + expect(registry.getAll()[1]).toBe(mockTopTask) + // Focus follows the replacement + expect(registry.current).toBe(mockTask2) }) it("marks a cancelled delegated child as interrupted and keeps parent delegated (preserving resume path)", async () => { @@ -552,7 +619,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -561,12 +628,12 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.resolve({ historyItem: parentHistory }) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -618,7 +685,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -627,12 +694,12 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.reject(new Error("parent lookup failed")) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -655,7 +722,7 @@ describe("ClineProvider flicker-free cancel", () => { rootTask: undefined, }), ) - expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + expect(provider["cancelledDelegationChildIds"].has("child-1")).toBe(true) }) it("does not rehydrate a cancelled child when standalone persistence also fails", async () => { @@ -683,7 +750,7 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") { return Promise.resolve({ historyItem: childHistory }) @@ -692,16 +759,16 @@ describe("ClineProvider flicker-free cancel", () => { return Promise.reject(new Error("parent lookup failed")) } throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] vi.spyOn(provider, "updateTaskHistory").mockRejectedValue(new Error("standalone persist failed")) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await expect(provider.cancelTask()).rejects.toThrow("standalone persist failed") expect(createTaskWithHistoryItemSpy).not.toHaveBeenCalled() - expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + expect(provider["cancelledDelegationChildIds"].has("child-1")).toBe(true) }) it("marks a cancelled delegated child as 'interrupted' and keeps parent delegated", async () => { @@ -745,17 +812,17 @@ describe("ClineProvider flicker-free cancel", () => { didFinishAbortingStream: true, isWaitingForFirstChunk: false, }) - ;(provider as any).clineStack = [mockTask1] + seedRegistry(provider, mockTask1) provider.getTaskWithId = vi.fn().mockImplementation((id) => { if (id === "child-1") return Promise.resolve({ historyItem: childHistory }) if (id === "parent-1") return Promise.resolve({ historyItem: parentHistory }) throw new Error(`unexpected task lookup: ${id}`) - }) as any + }) as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) const createTaskWithHistoryItemSpy = vi .spyOn(provider, "createTaskWithHistoryItem") - .mockResolvedValue(undefined as any) + .mockResolvedValue(undefined as unknown as CreatedHistoryTask) await provider.cancelTask() @@ -790,15 +857,15 @@ describe("ClineProvider flicker-free cancel", () => { emit: vi.fn(), abortTask: vi.fn().mockResolvedValue(undefined), } - ;(provider as any).clineStack = [childTask] - ;(provider as any).taskEventListeners = new Map() + seedRegistry(provider, childTask) + provider["taskEventListeners"] = new Map() - provider.getTaskWithId = vi.fn() as any + provider.getTaskWithId = vi.fn() as unknown as ClineProvider["getTaskWithId"] const updateTaskHistorySpy = vi.spyOn(provider, "updateTaskHistory").mockResolvedValue([]) - await (provider as any).removeClineFromStack() + await provider["removeClineFromStack"]() - expect((provider as any).clineStack).toHaveLength(0) + expect(provider["taskRegistry"].length).toBe(0) expect(childTask.abortTask).toHaveBeenCalledWith(true) // No history writes — lifecycle only expect(updateTaskHistorySpy).not.toHaveBeenCalled() diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index dbb4fe52d8..7e111442a9 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1,6 +1,7 @@ // pnpm --filter roo-cline test core/webview/__tests__/ClineProvider.spec.ts import * as path from "path" +import { TaskRegistry } from "../../task/TaskRegistry" import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" @@ -1457,7 +1458,7 @@ describe("ClineProvider", () => { test("handles case when no current task exists", async () => { // Clear the cline stack - ;(provider as any).clineStack = [] + ;(provider as any).taskRegistry = new TaskRegistry() // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json new file mode 100644 index 0000000000..a0f00c04e3 --- /dev/null +++ b/src/eslint-suppressions.json @@ -0,0 +1,1822 @@ +{ + "__mocks__/fs/promises.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/abandonSubtask.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "__tests__/api-subtask.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/delegation-concurrent.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "__tests__/delegation-events.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "__tests__/extension.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "__tests__/history-resume-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 72 + } + }, + "__tests__/migrateSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "__tests__/nested-delegation-resume.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "__tests__/new-task-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "__tests__/provider-delegation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "activate/CodeActionProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "activate/__tests__/CodeActionProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "activate/__tests__/handleUri.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "activate/__tests__/registerCommands.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "activate/registerCodeActions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "activate/registerCommands.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "activate/registerTerminalActions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/anthropic-vertex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "api/providers/__tests__/anthropic.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/base-openai-compatible-provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/base-provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/__tests__/bedrock-custom-arn.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "api/providers/__tests__/bedrock-error-handling.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/providers/__tests__/bedrock-inference-profiles.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 27 + } + }, + "api/providers/__tests__/bedrock-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "api/providers/__tests__/bedrock-reasoning.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/bedrock.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 38 + } + }, + "api/providers/__tests__/deepseek.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "api/providers/__tests__/gemini-handler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 18 + } + }, + "api/providers/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 33 + } + }, + "api/providers/__tests__/kenari.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/kimi-code.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/lite-llm.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 36 + } + }, + "api/providers/__tests__/lm-studio-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/lmstudio.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/mimo.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 29 + } + }, + "api/providers/__tests__/minimax.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/moonshot.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "api/providers/__tests__/native-ollama.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "api/providers/__tests__/openai-codex-native-tool-calls.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 30 + } + }, + "api/providers/__tests__/openai-codex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "api/providers/__tests__/openai-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "api/providers/__tests__/openai-native-usage.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "api/providers/__tests__/openai-native.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 79 + } + }, + "api/providers/__tests__/openai-timeout.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/openai-usage-tracking.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/openai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "api/providers/__tests__/opencode-go.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "api/providers/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "api/providers/__tests__/poe.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/qwen-code-native-tools.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/requesty.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/sambanova.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/unbound.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/__tests__/vertex.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/__tests__/vscode-lm.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/__tests__/xai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/__tests__/zai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/anthropic-vertex.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/anthropic.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/base-openai-compatible-provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/providers/base-provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/bedrock.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "api/providers/deepseek.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/fetchers/__tests__/kenari.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/kimi-code.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/lmstudio.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/fetchers/__tests__/modelCache.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/providers/fetchers/__tests__/modelEndpointCache.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/fetchers/__tests__/moonshot.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/ollama.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "api/providers/fetchers/__tests__/opencode-go.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/fetchers/__tests__/zoo-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/fetchers/litellm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/gemini.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/lite-llm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/providers/lm-studio.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/mimo.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/providers/moonshot.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/native-ollama.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/openai-codex.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "api/providers/openai-native.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "api/providers/openai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/openrouter.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/providers/poe.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/qwen-code.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/providers/requesty.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/unbound.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/utils/__tests__/error-handler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "api/providers/utils/__tests__/image-generation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "api/providers/utils/__tests__/timeout-config.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/providers/utils/error-handler.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/providers/vscode-lm.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/providers/xai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "api/transform/__tests__/ai-sdk.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/anthropic-filter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/__tests__/bedrock-converse-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/gemini-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/__tests__/mistral-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/__tests__/model-params.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/__tests__/openai-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 49 + } + }, + "api/transform/__tests__/r1-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/__tests__/reasoning.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/__tests__/responses-api-input.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/__tests__/responses-api-stream.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/__tests__/vscode-lm-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 23 + } + }, + "api/transform/__tests__/zai-format.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "api/transform/ai-sdk.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/bedrock-converse-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "api/transform/cache-strategy/__tests__/cache-strategy.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "api/transform/caching/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/gemini-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/openai-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "api/transform/r1-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "api/transform/responses-api-input.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "api/transform/responses-api-stream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "api/transform/vscode-lm-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "api/transform/zai-format.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/assistant-message/NativeToolCallParser.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/assistant-message/presentAssistantMessage.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/auto-approval/__tests__/AutoApprovalHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/checkpoints/__tests__/checkpoint.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/checkpoints/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/condense/__tests__/condense.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/condense/__tests__/foldedFileContext.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "core/condense/__tests__/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 24 + } + }, + "core/config/ContextProxy.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/config/CustomModesManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/ProviderSettingsManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/config/__tests__/ContextProxy.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/__tests__/CustomModesManager.exportImportSlugChange.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/config/__tests__/CustomModesManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/config/__tests__/CustomModesSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/config/__tests__/ModeConfig.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "core/config/__tests__/ProviderSettingsManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/context-management/__tests__/context-management.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/context-tracking/__tests__/FileContextTracker.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/context/context-management/__tests__/context-error-handling.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/context/context-management/context-error-handling.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/diff/stats.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/environment/__tests__/getEnvironmentDetails.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/ignore/__tests__/RooIgnoreController.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/mentions/__tests__/processUserContentMentions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/mentions/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/mentions/processUserContentMentions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/message-manager/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "core/message-manager/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/prompts/__tests__/add-custom-instructions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/__tests__/get-prompt-component.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/__tests__/responses-rooignore.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "core/prompts/__tests__/system-prompt.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/prompts/sections/__tests__/custom-instructions-global.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 18 + } + }, + "core/prompts/sections/__tests__/custom-instructions.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 55 + } + }, + "core/prompts/sections/__tests__/system-info.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "core/prompts/tools/filter-tools-for-mode.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/prompts/tools/native-tools/__tests__/converters.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/prompts/tools/native-tools/__tests__/read_file.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.crossInstance.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task-persistence/__tests__/TaskHistoryStore.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/task-persistence/__tests__/importRooTaskHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task-persistence/__tests__/taskMessages.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/task-persistence/apiMessages.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/task/Task.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "core/task/__tests__/Task.dispose.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/task/__tests__/Task.persistence.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/task/__tests__/Task.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 93 + } + }, + "core/task/__tests__/Task.sticky-profile-race.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/__tests__/Task.throttle.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 24 + } + }, + "core/task/__tests__/apiConversationHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 23 + } + }, + "core/task/__tests__/ask-clear-approval-buttons.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 19 + } + }, + "core/task/__tests__/ask-queued-message-drain.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 32 + } + }, + "core/task/__tests__/flushPendingToolResultsToHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 14 + } + }, + "core/task/__tests__/grace-retry-errors.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/task/__tests__/grounding-sources.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/task/__tests__/native-tools-filtering.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/__tests__/new-task-isolation.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/task/__tests__/reasoning-preservation.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "core/task/__tests__/task-tool-history.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/task/apiConversationHistory.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/tools/BaseTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/CodebaseSearchTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/GenerateImageTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/NewTaskTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/ReadFileTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/tools/ToolRepetitionDetector.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/UpdateTodoListTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/UseMcpToolTool.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/ReadCommandOutputTool.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/tools/__tests__/ToolRepetitionDetector.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/askFollowupQuestionTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "core/tools/__tests__/attemptCompletionTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "core/tools/__tests__/editFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/tools/__tests__/editTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/__tests__/executeCommand.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/tools/__tests__/executeCommandTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/tools/__tests__/generateImageTool.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "core/tools/__tests__/listFilesTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "core/tools/__tests__/mcpServerRestriction.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/tools/__tests__/newTaskTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 31 + } + }, + "core/tools/__tests__/readFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 98 + } + }, + "core/tools/__tests__/runSlashCommandTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/searchReplaceTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/skillTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/tools/__tests__/updateTodoListTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/useMcpToolTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 21 + } + }, + "core/tools/__tests__/validateToolUse.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/tools/__tests__/writeToFileTool.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/tools/helpers/toolResultFormatting.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/tools/validateToolUse.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "core/webview/ClineProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 34 + } + }, + "core/webview/__tests__/ClineProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 199 + } + }, + "core/webview/__tests__/ClineProvider.sticky-mode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 40 + } + }, + "core/webview/__tests__/ClineProvider.sticky-profile.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "core/webview/__tests__/ClineProvider.taskHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "core/webview/__tests__/checkpointRestoreHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/diagnosticsHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "core/webview/__tests__/messageEnhancer.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "core/webview/__tests__/skillsMessageHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/telemetrySettingsTracking.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "core/webview/__tests__/webviewMessageHandler.checkpoint.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "core/webview/__tests__/webviewMessageHandler.cloudAuth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/webviewMessageHandler.delete.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "core/webview/__tests__/webviewMessageHandler.edit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 13 + } + }, + "core/webview/__tests__/webviewMessageHandler.readFileContent.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 56 + } + }, + "core/webview/__tests__/webviewMessageHandler.searchFiles.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "core/webview/__tests__/webviewMessageHandler.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "core/webview/messageEnhancer.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "core/webview/webviewMessageHandler.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "extension.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "extension/__tests__/api-delete-queued-message.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "extension/__tests__/api-send-message.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "extension/api.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "i18n/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "i18n/setup.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/editor/DiffViewProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/editor/__tests__/DiffViewProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 311 + } + }, + "integrations/editor/__tests__/EditorUtils.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "integrations/kimi-code/__tests__/oauth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/misc/__tests__/export-markdown.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/misc/__tests__/extract-text.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "integrations/misc/__tests__/line-counter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/misc/__tests__/open-file.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 11 + } + }, + "integrations/misc/__tests__/performance/processCarriageReturns.benchmark.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "integrations/terminal/__tests__/OutputInterceptor.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "integrations/terminal/__tests__/TerminalProcess.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "integrations/terminal/__tests__/TerminalProcess.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/TerminalProcessInterpretExitCode.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "integrations/terminal/__tests__/TerminalProfile.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 35 + } + }, + "integrations/terminal/__tests__/TerminalRegistry.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "integrations/terminal/__tests__/setupTerminalTests.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/bashStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/cmdStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/terminal/__tests__/streamUtils/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "integrations/terminal/__tests__/streamUtils/pwshStream.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "integrations/theme/getTheme.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "services/__tests__/zoo-code-auth.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/checkpoints/__tests__/ShadowCheckpointService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/__tests__/config-manager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/__tests__/manager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 89 + } + }, + "services/code-index/__tests__/orchestrator.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "services/code-index/__tests__/service-factory.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 43 + } + }, + "services/code-index/embedders/__tests__/bedrock.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "services/code-index/embedders/__tests__/gemini.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/mistral.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/ollama.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 15 + } + }, + "services/code-index/embedders/__tests__/openai-compatible.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 28 + } + }, + "services/code-index/embedders/__tests__/openai.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "services/code-index/embedders/__tests__/openrouter.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/__tests__/vercel-ai-gateway.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/bedrock.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/embedders/ollama.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/embedders/openai-compatible.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/openai.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/embedders/openrouter.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/interfaces/vector-store.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/orchestrator.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/processors/__tests__/file-watcher.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "services/code-index/processors/__tests__/parser.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "services/code-index/processors/__tests__/scanner.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 26 + } + }, + "services/code-index/processors/file-watcher.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/processors/scanner.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/semble/__tests__/provider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 12 + } + }, + "services/code-index/semble/__tests__/semble-cli.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/semble/__tests__/semble-downloader.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 61 + } + }, + "services/code-index/semble/provider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/code-index/semble/semble-cli.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/code-index/semble/semble-downloader.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/code-index/shared/__tests__/validation-helpers.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/code-index/shared/validation-helpers.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/code-index/vector-store/__tests__/qdrant-client.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 87 + } + }, + "services/code-index/vector-store/qdrant-client.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/glob/__tests__/gitignore-integration.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/glob/__tests__/gitignore-test.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/glob/__tests__/list-files-limit.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "services/glob/__tests__/list-files.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 22 + } + }, + "services/marketplace/MarketplaceManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/marketplace/SimpleInstaller.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 7 + } + }, + "services/marketplace/__tests__/MarketplaceManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/marketplace/__tests__/SimpleInstaller.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 17 + } + }, + "services/marketplace/__tests__/marketplace-setting-check.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/mcp/McpHub.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 10 + } + }, + "services/mcp/McpOAuthClientProvider.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/mcp/McpServerManager.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/mcp/__tests__/McpHub.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 150 + } + }, + "services/mcp/__tests__/McpOAuthClientProvider.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "services/mcp/__tests__/SecretStorageService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/mcp/utils/__tests__/callbackServer.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/mcp/utils/__tests__/oauth.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 9 + } + }, + "services/mcp/utils/callbackServer.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/mcp/utils/oauth.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/mdm/__tests__/MdmService.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "services/ripgrep/__tests__/diagnostic.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "services/roo-config/__tests__/index.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 20 + } + }, + "services/roo-config/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/rules/__tests__/rules.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/search/__tests__/file-search.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "services/skills/__tests__/SkillsManager.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "services/tree-sitter/__tests__/helpers.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "services/tree-sitter/__tests__/markdownParser.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/__tests__/ProfileValidator.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/__tests__/api.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 8 + } + }, + "shared/__tests__/embeddingModels.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/__tests__/modes-empty-prompt-component.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/api.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "shared/checkExistApiConfig.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/cost.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/parse-command.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/support-prompt.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + } + }, + "shared/tools.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "shared/utils/__tests__/requesty.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/__tests__/autoImportSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 16 + } + }, + "utils/__tests__/enhance-prompt.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "utils/__tests__/git.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 95 + } + }, + "utils/__tests__/json-schema.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "utils/__tests__/migrateSettings.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "utils/__tests__/outputChannelLogger.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "utils/__tests__/safeWriteJson.test.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 27 + } + }, + "utils/__tests__/shell.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 46 + } + }, + "utils/__tests__/storage.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 25 + } + }, + "utils/__tests__/tiktoken.spec.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/config.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/export.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "utils/safeWriteJson.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 4 + } + }, + "utils/tts.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "vitest.setup.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + } +} diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index 5c866ef941..cea212fefa 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -13,7 +13,8 @@ export default [ "no-empty": "off", "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", + // Enforced; existing violations are suppressed in eslint-suppressions.json and cleaned up incrementally. + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-require-imports": "off", "@typescript-eslint/ban-ts-comment": "off", },