diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 458120d9a4..7bdf03f0a2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -144,7 +144,7 @@ function runDelegationTransition( locks.set(parentTaskId, tail) - tail.finally(() => { + void tail.finally(() => { if (locks.get(parentTaskId) === tail) { locks.delete(parentTaskId) } @@ -228,7 +228,7 @@ export class ClineProvider ClineProvider.activeInstances.add(this) this.mdmService = mdmService - this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) + void this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) // Initialize the per-task file-based history store. // The globalState write-through is debounced separately (not on every mutation) @@ -725,7 +725,7 @@ export class ClineProvider this.mcpHub = undefined await this.skillsManager?.dispose() this.skillsManager = undefined - this.marketplaceManager?.cleanup() + await this.marketplaceManager?.cleanup() this.customModesManager?.dispose() this.taskHistoryStore.dispose() this.flushGlobalStateWriteThrough() @@ -860,9 +860,27 @@ export class ClineProvider setPanel(webviewView, "sidebar") } + // Set up webview options with proper resource roots + const resourceRoots = [this.contextProxy.extensionUri] + + // Add workspace folders to allow access to workspace files + if (vscode.workspace.workspaceFolders) { + resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri)) + } + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: resourceRoots, + } + + webviewView.webview.html = + this.contextProxy.extensionMode === vscode.ExtensionMode.Development + ? await this.getHMRHtmlContent(webviewView.webview) + : await this.getHtmlContent(webviewView.webview) + // Initialize out-of-scope variables that need to receive persistent // global state values. - this.getState().then( + await this.getState().then( ({ terminalShellIntegrationTimeout = Terminal.defaultShellIntegrationTimeout, terminalShellIntegrationDisabled = false, @@ -890,24 +908,6 @@ export class ClineProvider }, ) - // Set up webview options with proper resource roots - const resourceRoots = [this.contextProxy.extensionUri] - - // Add workspace folders to allow access to workspace files - if (vscode.workspace.workspaceFolders) { - resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri)) - } - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: resourceRoots, - } - - webviewView.webview.html = - this.contextProxy.extensionMode === vscode.ExtensionMode.Development - ? await this.getHMRHtmlContent(webviewView.webview) - : await this.getHtmlContent(webviewView.webview) - // Sets up an event listener to listen for messages passed from the webview view context // and executes code based on the message that is received. this.setWebviewMessageListener(webviewView.webview) @@ -930,7 +930,7 @@ export class ClineProvider // for this visibility listener panel. const viewStateDisposable = webviewView.onDidChangeViewState(() => { if (this.view?.visible) { - this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) + void this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) } else { this.logWebviewHiddenDiagnostics() } @@ -941,7 +941,7 @@ export class ClineProvider // sidebar const visibilityDisposable = webviewView.onDidChangeVisibility(() => { if (this.view?.visible) { - this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) + void this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) } else { this.logWebviewHiddenDiagnostics() } @@ -2139,7 +2139,7 @@ export class ClineProvider const state = await this.getStateToPostToWebview() this.clineMessagesSeq++ state.clineMessagesSeq = this.clineMessagesSeq - this.postMessageToWebview({ type: "state", state }) + await this.postMessageToWebview({ type: "state", state }) } /** @@ -2155,7 +2155,7 @@ export class ClineProvider this.clineMessagesSeq++ state.clineMessagesSeq = this.clineMessagesSeq const { taskHistory: _omit, ...rest } = state - this.postMessageToWebview({ type: "state", state: rest }) + await this.postMessageToWebview({ type: "state", state: rest }) } /** @@ -2172,7 +2172,7 @@ export class ClineProvider async postStateToWebviewWithoutClineMessages(): Promise { const state = await this.getStateToPostToWebview() const { clineMessages: _omitMessages, taskHistory: _omitHistory, ...rest } = state - this.postMessageToWebview({ type: "state", state: rest }) + await this.postMessageToWebview({ type: "state", state: rest }) } /** @@ -2192,7 +2192,7 @@ export class ClineProvider ]) // Send marketplace data separately - this.postMessageToWebview({ + await this.postMessageToWebview({ type: "marketplaceData", organizationMcps: marketplaceResult.organizationMcps || [], marketplaceItems: marketplaceResult.marketplaceItems || [], @@ -2203,7 +2203,7 @@ export class ClineProvider console.error("Failed to fetch marketplace data:", error) // Send empty data on error to prevent UI from hanging - this.postMessageToWebview({ + await this.postMessageToWebview({ type: "marketplaceData", organizationMcps: [], marketplaceItems: [], @@ -3002,7 +3002,7 @@ export class ClineProvider if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) { // Get the full status from the manager to ensure we have all fields correctly formatted const fullStatus = currentManager.getCurrentStatus() - this.postMessageToWebview({ + void this.postMessageToWebview({ type: "indexingStatusUpdate", values: fullStatus, }) @@ -3014,7 +3014,7 @@ export class ClineProvider } // Send initial status for the current workspace - this.postMessageToWebview({ + void this.postMessageToWebview({ type: "indexingStatusUpdate", values: currentManager.getCurrentStatus(), }) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 93f4f2afaa..1e2c0402bf 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -12,6 +12,7 @@ import { type ClineMessage, type ExtensionMessage, type ExtensionState, + type WebviewMessage, ORGANIZATION_ALLOW_ALL, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, @@ -28,6 +29,7 @@ import { Task, TaskOptions } from "../../task/Task" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" +import { webviewMessageHandler } from "../webviewMessageHandler" import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../../api/providers/fetchers/lmstudio" @@ -169,6 +171,8 @@ vi.mock("vscode", () => ({ showInformationMessage: vi.fn(), showWarningMessage: vi.fn(), showErrorMessage: vi.fn(), + showSaveDialog: vi.fn(), + showOpenDialog: vi.fn(), activeTextEditor: undefined, onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), }, @@ -202,8 +206,39 @@ vi.mock("vscode", () => ({ })) vi.mock("../../../utils/tts", () => ({ + playTts: vi.fn().mockResolvedValue(undefined), setTtsEnabled: vi.fn(), setTtsSpeed: vi.fn(), + stopTts: vi.fn(), +})) + +vi.mock("../../../integrations/misc/open-file", () => ({ + openFile: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../integrations/misc/image-handler", () => ({ + openImage: vi.fn().mockResolvedValue(undefined), + saveImage: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../mentions", () => ({ + openMention: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../utils/export", () => ({ + resolveDefaultSaveUri: vi.fn().mockResolvedValue({ fsPath: "/test/default-export.yaml" }), + saveLastExportPath: vi.fn().mockResolvedValue(undefined), +})) + +vi.mock("../../../integrations/openai-codex/oauth", () => ({ + openAiCodexOAuthManager: { + getAccessToken: vi.fn(), + getAccountId: vi.fn(), + }, +})) + +vi.mock("../../../integrations/openai-codex/rate-limits", () => ({ + fetchOpenAiCodexRateLimitInfo: vi.fn(), })) vi.mock("../../../api", () => ({ @@ -219,7 +254,7 @@ vi.mock("../../../integrations/workspace/WorkspaceTracker", () => { return { default: vi.fn().mockImplementation(function () { return { - initializeFilePaths: vi.fn(), + initializeFilePaths: vi.fn().mockResolvedValue(undefined), dispose: vi.fn(), } }), @@ -359,6 +394,8 @@ vi.mock("@roo-code/cloud", () => ({ get instance() { return { isAuthenticated: vi.fn().mockReturnValue(false), + login: vi.fn().mockResolvedValue(undefined), + logout: vi.fn().mockResolvedValue(undefined), off: vi.fn(), } }, @@ -785,7 +822,8 @@ describe("ClineProvider", () => { await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as ReturnType).mock + .calls[0][0] // Simulate webviewDidLaunch message await messageHandler({ type: "webviewDidLaunch" }) @@ -794,6 +832,29 @@ describe("ClineProvider", () => { expect(mockPostMessage).toHaveBeenCalled() }) + test("logs detached workspace initialization failures", async () => { + await provider.resolveWebviewView(mockWebviewView) + + let rejectInitialization!: (error: Error) => void + const initializationPromise = new Promise((_, reject) => { + rejectInitialization = reject + }) + const initializeSpy = vi + .spyOn(provider.workspaceTracker!, "initializeFilePaths") + .mockReturnValue(initializationPromise) + const logSpy = vi.spyOn(provider, "log") + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + + await expect(messageHandler({ type: "webviewDidLaunch" })).resolves.toBeUndefined() + expect(initializeSpy).toHaveBeenCalledOnce() + + rejectInitialization(new Error("workspace boom")) + await Promise.resolve() + await Promise.resolve() + + expect(logSpy).toHaveBeenCalledWith("Workspace initialization error: Error: workspace boom") + }) + test("clearTask aborts current task", async () => { // Setup Cline instance with auto-mock from the top of the file const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance @@ -1183,7 +1244,7 @@ describe("ClineProvider", () => { setModeConfig: vi.fn(), } as any - provider.setValue("currentApiConfigName", "current-config") + await provider.setValue("currentApiConfigName", "current-config") // Switch to architect mode await messageHandler({ type: "mode", text: "architect" }) @@ -1281,7 +1342,7 @@ describe("ClineProvider", () => { }, } - provider.setValue("customModePrompts", existingPrompts) + await provider.setValue("customModePrompts", existingPrompts) // Test updating a prompt await messageHandler({ @@ -2273,6 +2334,397 @@ describe("ClineProvider", () => { }) }) +describe("webviewMessageHandler no-floating-promises coverage", () => { + const createProvider = (overrides: Record = {}) => + Object.assign( + { + context: { + secrets: { + get: vi.fn().mockResolvedValue(undefined), + }, + }, + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn().mockResolvedValue(undefined), + }, + postMessageToWebview: vi.fn().mockResolvedValue(true), + postStateToWebview: vi.fn().mockResolvedValue(undefined), + getCurrentTask: vi.fn(), + getCurrentWorkspaceCodeIndexManager: vi.fn(), + getMcpHub: vi.fn().mockReturnValue({ + getMcpSettingsFilePath: vi.fn().mockResolvedValue("/test/mcp.json"), + }), + providerSettingsManager: { + listConfig: vi.fn().mockResolvedValue([]), + }, + customModesManager: { + getCustomModesFilePath: vi.fn().mockResolvedValue("/test/custom-modes.yaml"), + exportModeWithRules: vi.fn(), + importModeWithRules: vi.fn(), + getCustomModes: vi.fn().mockResolvedValue([]), + checkRulesDirectoryHasContent: vi.fn().mockResolvedValue(true), + }, + exportTaskWithId: vi.fn().mockResolvedValue(undefined), + showTaskWithId: vi.fn().mockResolvedValue(undefined), + condenseTaskContext: vi.fn().mockResolvedValue(undefined), + deleteTaskWithId: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + cwd: "/test/workspace", + }, + overrides, + ) as unknown as ClineProvider + + const createIndexManager = (overrides: Record = {}) => + Object.assign( + { + setWorkspaceEnabled: vi.fn().mockResolvedValue(undefined), + setAutoEnableDefault: vi.fn().mockResolvedValue(undefined), + isFeatureEnabled: true, + isFeatureConfigured: true, + isWorkspaceEnabled: true, + initialize: vi.fn().mockResolvedValue(undefined), + state: "Standby", + isInitialized: true, + startIndexing: vi.fn().mockResolvedValue(undefined), + stopIndexing: vi.fn(), + clearIndexData: vi.fn().mockResolvedValue(undefined), + getCurrentStatus: vi.fn().mockReturnValue({ systemStatus: "Standby" }), + }, + overrides, + ) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("logs a detached indexing rejection without rejecting the handler", async () => { + let rejectIndexing!: (error: Error) => void + const indexingPromise = new Promise((_, reject) => { + rejectIndexing = reject + }) + const manager = createIndexManager({ + startIndexing: vi.fn().mockReturnValue(indexingPromise), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await expect(webviewMessageHandler(provider, { type: "startIndexing" })).resolves.toBeUndefined() + expect(manager.startIndexing).toHaveBeenCalledOnce() + + rejectIndexing(new Error("boom")) + await Promise.resolve() + await Promise.resolve() + + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: boom") + }) + + it("covers the changed task-operation happy paths", async () => { + const task = { + taskId: "task-1", + handleTerminalOperation: vi.fn().mockResolvedValue(undefined), + } + const provider = createProvider({ getCurrentTask: vi.fn().mockReturnValue(task) }) + + await webviewMessageHandler(provider, { type: "terminalOperation", terminalOperation: "continue" }) + await webviewMessageHandler(provider, { type: "exportCurrentTask" }) + await webviewMessageHandler(provider, { type: "showTaskWithId", text: "task-2" }) + await webviewMessageHandler(provider, { type: "condenseTaskContextRequest", text: "task-2" }) + await webviewMessageHandler(provider, { type: "deleteTaskWithId", text: "task-2" }) + await webviewMessageHandler(provider, { type: "exportTaskWithId", text: "task-2" }) + + expect(task.handleTerminalOperation).toHaveBeenCalledWith("continue") + expect(provider.exportTaskWithId).toHaveBeenCalledTimes(2) + expect(provider.showTaskWithId).toHaveBeenCalledWith("task-2") + expect(provider.condenseTaskContext).toHaveBeenCalledWith("task-2") + expect(provider.deleteTaskWithId).toHaveBeenCalledWith("task-2") + }) + + it("covers changed file, image, mention, settings, and TTS dispatch paths", async () => { + const { openFile } = await import("../../../integrations/misc/open-file") + const { openImage, saveImage } = await import("../../../integrations/misc/image-handler") + const { openMention } = await import("../../mentions") + const { playTts } = await import("../../../utils/tts") + const provider = createProvider() + + await webviewMessageHandler(provider, { type: "openImage", text: "/test/image.png" }) + await webviewMessageHandler(provider, { type: "saveImage", dataUri: "invalid" }) + await webviewMessageHandler(provider, { type: "openFile", text: "/test/file.ts" }) + await webviewMessageHandler(provider, { type: "openMention", text: "file.ts" }) + await webviewMessageHandler(provider, { type: "openCustomModesSettings" }) + await webviewMessageHandler(provider, { type: "openMcpSettings" }) + await webviewMessageHandler(provider, { type: "playTts", text: "hello" }) + + expect(openImage).toHaveBeenCalledWith("/test/image.png", { values: undefined }) + expect(saveImage).toHaveBeenCalledOnce() + expect(openFile).toHaveBeenCalledTimes(3) + expect(openMention).toHaveBeenCalledWith("/test/workspace", "file.ts") + expect(playTts).toHaveBeenCalledOnce() + }) + + it("covers changed configuration and rules response paths", async () => { + const provider = createProvider() + + await webviewMessageHandler(provider, { type: "getListApiConfiguration" }) + await webviewMessageHandler(provider, { type: "checkRulesDirectory", slug: "mode-1" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "listApiConfig", listApiConfig: [] }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "checkRulesDirectoryResult", + slug: "mode-1", + hasContent: true, + }) + }) + + it("covers all changed export-mode response paths", async () => { + const provider = createProvider() + const exportModeWithRules = provider.customModesManager.exportModeWithRules as ReturnType + const showSaveDialog = vi.mocked(vscode.window.showSaveDialog) + + exportModeWithRules.mockResolvedValueOnce({ success: true, yaml: "mode: one" }) + showSaveDialog.mockResolvedValueOnce({ fsPath: "/test/mode.yaml" } as vscode.Uri) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockResolvedValueOnce({ success: true, yaml: "mode: one" }) + showSaveDialog.mockResolvedValueOnce(undefined) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockResolvedValueOnce({ success: false, error: "invalid mode" }) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + exportModeWithRules.mockRejectedValueOnce(new Error("export failed")) + await webviewMessageHandler(provider, { type: "exportMode", slug: "mode-1" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "Export cancelled" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "invalid mode" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportModeResult", error: "export failed" }), + ) + }) + + it("covers all changed import-mode response paths", async () => { + const provider = createProvider() + const importModeWithRules = provider.customModesManager.importModeWithRules as ReturnType + const showOpenDialog = vi.mocked(vscode.window.showOpenDialog) + const selectedFile = [{ fsPath: "/test/mode.yaml" } as vscode.Uri] + + showOpenDialog.mockResolvedValueOnce(selectedFile) + importModeWithRules.mockResolvedValueOnce({ success: true, slug: "mode-1" }) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockResolvedValueOnce(selectedFile) + importModeWithRules.mockResolvedValueOnce({ success: false, error: "invalid mode" }) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockResolvedValueOnce(undefined) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + showOpenDialog.mockRejectedValueOnce(new Error("dialog failed")) + await webviewMessageHandler(provider, { type: "importMode", source: "project" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "invalid mode" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "cancelled" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "importModeResult", error: "dialog failed" }), + ) + }) + + it("covers changed cloud sign-out and rate-limit error responses", async () => { + const { CloudService } = await import("@roo-code/cloud") + const { openAiCodexOAuthManager } = await import("../../../integrations/openai-codex/oauth") + const provider = createProvider() + + vi.mocked(CloudService.hasInstance).mockReturnValueOnce(false) + await webviewMessageHandler(provider, { type: "rooCloudSignOut" }) + await webviewMessageHandler(provider, { type: "rooCloudSignOut" }) + + vi.mocked(openAiCodexOAuthManager.getAccessToken).mockRejectedValueOnce(new Error("token failed")) + await webviewMessageHandler(provider, { type: "requestOpenAiCodexRateLimits" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "openAiCodexRateLimits", + error: "token failed", + }) + }) + + it("covers changed indexing status, secret, and missing-manager responses", async () => { + const manager = createIndexManager() + const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager) + const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager }) + + await webviewMessageHandler(provider, { type: "requestIndexingStatus" }) + await webviewMessageHandler(provider, { type: "requestIndexingStatus" }) + await webviewMessageHandler(provider, { type: "requestCodeIndexSecretStatus" }) + getManager.mockReturnValueOnce(undefined) + await webviewMessageHandler(provider, { type: "startIndexing" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "codeIndexSecretStatus" }), + ) + expect(provider.log).toHaveBeenCalledWith("Cannot start indexing: No workspace folder open") + }) + + it("catches both start-indexing calls during error recovery", async () => { + const manager = createIndexManager({ + isInitialized: false, + startIndexing: vi + .fn() + .mockRejectedValueOnce(new Error("first failure")) + .mockRejectedValueOnce(new Error("second failure")), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await webviewMessageHandler(provider, { type: "startIndexing" }) + await Promise.resolve() + + expect(manager.startIndexing).toHaveBeenCalledTimes(2) + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: first failure") + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: second failure") + }) + + it("covers changed stop, toggle, and detached toggle rejection paths", async () => { + const manager = createIndexManager({ + startIndexing: vi.fn().mockRejectedValue(new Error("toggle failure")), + }) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + await webviewMessageHandler(provider, { type: "stopIndexing" }) + await webviewMessageHandler(provider, { type: "toggleWorkspaceIndexing", bool: true }) + await Promise.resolve() + + expect(manager.stopIndexing).toHaveBeenCalledOnce() + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: toggle failure") + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "indexingStatusUpdate" }), + ) + }) + + it("catches auto-enabled indexing failures and posts the resulting status", async () => { + const { CodeIndexManager } = await import("../../../services/code-index/manager") + let workspaceEnabled = false + const manager = createIndexManager({ + setAutoEnableDefault: vi.fn().mockImplementation(async () => { + workspaceEnabled = true + }), + startIndexing: vi.fn().mockRejectedValue(new Error("auto-enable failure")), + }) + Object.defineProperty(manager, "isWorkspaceEnabled", { get: () => workspaceEnabled }) + const getAllInstances = vi + .spyOn(CodeIndexManager, "getAllInstances") + .mockReturnValue([manager] as unknown as ReturnType) + const provider = createProvider({ + getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager), + }) + + try { + await webviewMessageHandler(provider, { type: "setAutoEnableDefault", bool: true }) + await Promise.resolve() + + expect(manager.startIndexing).toHaveBeenCalledOnce() + expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: auto-enable failure") + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "indexingStatusUpdate" }), + ) + } finally { + getAllInstances.mockRestore() + } + }) + + it("covers changed clear-index response paths", async () => { + const manager = createIndexManager() + const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager) + const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager }) + + await webviewMessageHandler(provider, { type: "clearIndexData" }) + await webviewMessageHandler(provider, { type: "clearIndexData" }) + manager.clearIndexData.mockRejectedValueOnce(new Error("clear failed")) + await webviewMessageHandler(provider, { type: "clearIndexData" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "indexCleared", + values: { success: true }, + }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "indexCleared", + values: { success: false, error: "clear failed" }, + }) + }) + + it("covers changed marketplace error and removal responses", async () => { + const provider = createProvider() + const item = { + id: "item-1", + name: "Item 1", + description: "Test marketplace item", + type: "mode", + content: "slug: item-1", + } satisfies NonNullable + const options = { target: "project" } satisfies NonNullable + const marketplaceManager = { + installMarketplaceItem: vi.fn().mockRejectedValue(new Error("install failed")), + removeInstalledMarketplaceItem: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error("remove failed")), + } + const managerArgument = marketplaceManager as unknown as NonNullable< + Parameters[2] + > + + await webviewMessageHandler( + provider, + { type: "installMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler( + provider, + { type: "removeInstalledMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler( + provider, + { type: "removeInstalledMarketplaceItem", mpItem: item, mpInstallOptions: options }, + managerArgument, + ) + await webviewMessageHandler(provider, { + type: "removeInstalledMarketplaceItem", + mpItem: item, + mpInstallOptions: options, + }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceInstallResult", success: false }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", success: true }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", error: "remove failed" }), + ) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "marketplaceRemoveResult", error: "Marketplace manager is not available" }), + ) + }) +}) + describe("Project MCP Settings", () => { let provider: ClineProvider let mockContext: vscode.ExtensionContext diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index a3f62f74f3..5a517cdcc0 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -31,7 +31,7 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore // This prevents "Current ask promise was ignored" errors // For edit operations, we don't abort because the checkpoint restore will handle it if (operation === "delete" && currentCline && !currentCline.abort) { - currentCline.abortTask() + await currentCline.abortTask() // Wait a bit for the abort to complete await pWaitFor(() => currentCline.abort === true, { timeout: 1000, diff --git a/src/core/webview/rulesMessageHandler.ts b/src/core/webview/rulesMessageHandler.ts index 343adf8738..68fcc5f7b2 100644 --- a/src/core/webview/rulesMessageHandler.ts +++ b/src/core/webview/rulesMessageHandler.ts @@ -38,7 +38,7 @@ export async function handleCreateRule( try { const input = parseCreateRuleInput(message) const createdPath = await createRule(cwd, input) - openFile(createdPath) + await openFile(createdPath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error creating rule: ${errorMessage}`) @@ -89,7 +89,7 @@ export async function handleOpenRuleFile(provider: ClineProvider, cwd: string, m throw new Error("Rule file not found") } - openFile(filePath) + await openFile(filePath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error opening rule file: ${errorMessage}`) @@ -109,7 +109,7 @@ export async function handleOpenRulesDirectory( kind: values.kind, modeSlug: values.modeSlug, } as CreateRuleInput) - openFile(directoryPath) + await openFile(directoryPath) } catch (error) { const errorMessage = getErrorMessage(error) provider.log(`Error opening rules directory: ${errorMessage}`) diff --git a/src/core/webview/skillsMessageHandler.ts b/src/core/webview/skillsMessageHandler.ts index 496ff70c24..2eab270734 100644 --- a/src/core/webview/skillsMessageHandler.ts +++ b/src/core/webview/skillsMessageHandler.ts @@ -55,7 +55,7 @@ export async function handleCreateSkill( const createdPath = await skillsManager.createSkill(skillName, source, skillDescription, modeSlugs) // Open the created file in the editor - openFile(createdPath) + await openFile(createdPath) // Send updated skills list const skills = skillsManager.getSkillsMetadata() @@ -199,7 +199,7 @@ export async function handleOpenSkillFile(provider: ClineProvider, message: Webv throw new Error(t("skills:errors.skill_not_found", { name: skillName })) } - openFile(skill.path) + await openFile(skill.path) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error opening skill file: ${errorMessage}`) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3126d4ebf5..7009343573 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -563,17 +563,21 @@ export const webviewMessageHandler = async ( const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) - provider.postStateToWebview() - provider.workspaceTracker?.initializeFilePaths() // Don't await. + await provider.postStateToWebview() + void provider.workspaceTracker + ?.initializeFilePaths() + .catch((err) => provider.log(`Workspace initialization error: ${err}`)) // Don't await. - getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })) + await getTheme().then((theme) => + provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }), + ) // If MCP Hub is already initialized, update the webview with // current server list. const mcpHub = provider.getMcpHub() if (mcpHub) { - provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() }) + await provider.postMessageToWebview({ type: "mcpServers", mcpServers: mcpHub.getAllServers() }) } provider.providerSettingsManager @@ -630,7 +634,7 @@ export const webviewMessageHandler = async ( ) // Enable telemetry by default (when unset) or when explicitly enabled - provider.getStateToPostToWebview().then((state) => { + await provider.getStateToPostToWebview().then((state) => { const { telemetrySetting } = state const isOptedIn = telemetrySetting !== "disabled" TelemetryService.instance.updateTelemetryState(isOptedIn) @@ -786,7 +790,7 @@ export const webviewMessageHandler = async ( case "terminalOperation": if (message.terminalOperation) { - provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) + await provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) } break case "clearTask": @@ -812,7 +816,7 @@ export const webviewMessageHandler = async ( case "exportCurrentTask": const currentTaskId = provider.getCurrentTask()?.taskId if (currentTaskId) { - provider.exportTaskWithId(currentTaskId) + await provider.exportTaskWithId(currentTaskId) } break case "shareCurrentTask": @@ -826,13 +830,13 @@ export const webviewMessageHandler = async ( vscode.window.showErrorMessage(t("common:errors.share_not_enabled")) break case "showTaskWithId": - provider.showTaskWithId(message.text!) + await provider.showTaskWithId(message.text!) break case "condenseTaskContextRequest": - provider.condenseTaskContext(message.text!) + await provider.condenseTaskContext(message.text!) break case "deleteTaskWithId": - provider.deleteTaskWithId(message.text!) + await provider.deleteTaskWithId(message.text!) break case "abandonSubtaskWithId": provider @@ -888,7 +892,7 @@ export const webviewMessageHandler = async ( break } case "exportTaskWithId": - provider.exportTaskWithId(message.text!) + await provider.exportTaskWithId(message.text!) break case "getTaskWithAggregatedCosts": { try { @@ -1245,7 +1249,7 @@ export const webviewMessageHandler = async ( routerModels[routerName] = {} // Ensure it's an empty object in the main routerModels message. - provider.postMessageToWebview({ + void provider.postMessageToWebview({ type: "singleRouterModelFetchResponse", success: false, error: errorMessage, @@ -1254,7 +1258,7 @@ export const webviewMessageHandler = async ( } }) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "routerModels", routerModels, values: providerFilter ? { provider: requestedProvider } : undefined, @@ -1284,7 +1288,7 @@ export const webviewMessageHandler = async ( } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) provider.log(`[requestOllamaModels] Failed to refresh model cache for ${logBaseUrl}: ${errorMsg}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: {}, error: errorMsg, @@ -1297,11 +1301,11 @@ export const webviewMessageHandler = async ( // Always post a response so the webview refresh status can // transition out of "loading" — even when no models are found. - provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) provider.log(`[requestOllamaModels] Failed to read models for ${logBaseUrl}: ${errorMsg}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: {}, error: errorMsg, @@ -1329,7 +1333,7 @@ export const webviewMessageHandler = async ( } if (Object.keys(lmStudioModels).length > 0) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels: lmStudioModels, }) @@ -1341,7 +1345,7 @@ export const webviewMessageHandler = async ( break } case "requestRooModels": { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "singleRouterModelFetchResponse", success: false, error: getRouterRemovalMessage(), @@ -1357,24 +1361,24 @@ export const webviewMessageHandler = async ( message?.values?.openAiHeaders, ) - provider.postMessageToWebview({ type: "openAiModels", openAiModels }) + await provider.postMessageToWebview({ type: "openAiModels", openAiModels }) } break case "requestVsCodeLmModels": const vsCodeLmModels = await getVsCodeLmModels() // TODO: Cache like we do for OpenRouter, etc? - provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) + await provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "openImage": - openImage(message.text!, { values: message.values }) + await openImage(message.text!, { values: message.values }) break case "saveImage": if (message.dataUri) { const matches = message.dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) if (!matches) { // Let saveImage handle invalid URI error - saveImage(message.dataUri, vscode.Uri.file("")) + await saveImage(message.dataUri, vscode.Uri.file("")) break } const format = matches[1] @@ -1402,12 +1406,12 @@ export const webviewMessageHandler = async ( if (!path.isAbsolute(filePath)) { filePath = path.join(getCurrentCwd(), filePath) } - openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) + await openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) break case "readFileContent": { const relPath = message.text || "" if (!relPath) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "No path provided" }, }) @@ -1416,7 +1420,7 @@ export const webviewMessageHandler = async ( try { const cwd = getCurrentCwd() if (!cwd) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "No workspace path available" }, }) @@ -1425,17 +1429,17 @@ export const webviewMessageHandler = async ( const absPath = path.resolve(cwd, relPath) // Workspace-boundary validation: prevent path traversal attacks if (isPathOutsideWorkspace(absPath)) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: "Path is outside workspace" }, }) break } const content = await fs.readFile(absPath, "utf-8") - provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content } }) + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content } }) } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "fileContent", fileContent: { path: relPath, content: null, error: errorMsg }, }) @@ -1443,7 +1447,7 @@ export const webviewMessageHandler = async ( break } case "openMention": - openMention(getCurrentCwd(), message.text) + await openMention(getCurrentCwd(), message.text) break case "openExternal": if (message.url) { @@ -1573,7 +1577,7 @@ export const webviewMessageHandler = async ( const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() if (customModesFilePath) { - openFile(customModesFilePath) + await openFile(customModesFilePath) } break @@ -1600,7 +1604,7 @@ export const webviewMessageHandler = async ( const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath() if (mcpSettingsFilePath) { - openFile(mcpSettingsFilePath) + await openFile(mcpSettingsFilePath) } break @@ -1737,7 +1741,7 @@ export const webviewMessageHandler = async ( break case "playTts": if (message.text) { - playTts(message.text, { + void playTts(message.text, { onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }), onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }), }) @@ -1820,7 +1824,7 @@ export const webviewMessageHandler = async ( customModePrompts: updatedPrompts, hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false, } - provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) + await provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) if (TelemetryService.hasInstance()) { // Determine which setting was changed by comparing objects @@ -2225,7 +2229,7 @@ export const webviewMessageHandler = async ( try { const listApiConfig = await provider.providerSettingsManager.listConfig() await updateGlobalState("listApiConfigMeta", listApiConfig) - provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) + await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) } catch (error) { provider.log( `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, @@ -2401,7 +2405,7 @@ export const webviewMessageHandler = async ( await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8") // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: true, slug: message.slug, @@ -2411,7 +2415,7 @@ export const webviewMessageHandler = async ( vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug })) } else { // User cancelled the save dialog - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: "Export cancelled", @@ -2420,7 +2424,7 @@ export const webviewMessageHandler = async ( } } else { // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: result.error, @@ -2432,7 +2436,7 @@ export const webviewMessageHandler = async ( provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "exportModeResult", success: false, error: errorMessage, @@ -2491,7 +2495,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() // Send success message to webview, include the imported slug so UI can switch - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: true, slug: result.slug, @@ -2501,7 +2505,7 @@ export const webviewMessageHandler = async ( vscode.window.showInformationMessage(t("common:info.mode_imported")) } else { // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: result.error, @@ -2512,7 +2516,7 @@ export const webviewMessageHandler = async ( } } else { // User cancelled the file dialog - reset the importing state - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: "cancelled", @@ -2523,7 +2527,7 @@ export const webviewMessageHandler = async ( provider.log(`Failed to import mode: ${errorMessage}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "importModeResult", success: false, error: errorMessage, @@ -2537,7 +2541,7 @@ export const webviewMessageHandler = async ( if (message.slug) { const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "checkRulesDirectoryResult", slug: message.slug, hasContent: hasContent, @@ -2615,14 +2619,14 @@ export const webviewMessageHandler = async ( case "rooCloudSignOut": { if (!isCloudServiceAvailable()) { await provider.postStateToWebview() - provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) + await provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) break } try { await CloudService.instance.logout() await provider.postStateToWebview() - provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) + await provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) } catch (error) { provider.log(`AuthService#logout failed: ${error}`) vscode.window.showErrorMessage("Sign out failed.") @@ -3025,7 +3029,7 @@ export const webviewMessageHandler = async ( const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", @@ -3050,7 +3054,7 @@ export const webviewMessageHandler = async ( workspacePath: undefined, } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: status, }) @@ -3070,7 +3074,7 @@ export const webviewMessageHandler = async ( )) const hasOpenRouterApiKey = !!(await provider.context.secrets.get("codebaseIndexOpenRouterApiKey")) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "codeIndexSecretStatus", values: { hasOpenAiKey, @@ -3088,7 +3092,7 @@ export const webviewMessageHandler = async ( try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { systemStatus: "Error", @@ -3110,12 +3114,12 @@ export const webviewMessageHandler = async ( const currentState = manager.state if (currentState === "Standby" || currentState === "Error") { - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) if (manager.state === "Standby" || manager.state === "Error") { - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } } } @@ -3133,7 +3137,7 @@ export const webviewMessageHandler = async ( return } manager.stopIndexing() - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3153,11 +3157,11 @@ export const webviewMessageHandler = async ( await manager.setWorkspaceEnabled(enabled) if (enabled && manager.isFeatureEnabled && manager.isFeatureConfigured) { await manager.initialize(provider.contextProxy) - manager.startIndexing() + void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } else if (!enabled) { manager.stopIndexing() } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3187,10 +3191,10 @@ export const webviewMessageHandler = async ( m.stopIndexing() } else if (!wasEnabled && isNowEnabled && m.isFeatureEnabled && m.isFeatureConfigured) { await m.initialize(provider.contextProxy) - m.startIndexing() + void m.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`)) } } - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexingStatusUpdate", values: manager.getCurrentStatus(), }) @@ -3206,7 +3210,7 @@ export const webviewMessageHandler = async ( const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { provider.log("Cannot clear index data: No workspace folder open") - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexCleared", values: { success: false, @@ -3216,10 +3220,10 @@ export const webviewMessageHandler = async ( return } await manager.clearIndexData() - provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) + await provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) } catch (error) { provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "indexCleared", values: { success: false, @@ -3268,7 +3272,7 @@ export const webviewMessageHandler = async ( console.log(`Marketplace item installed and config file opened: ${configFilePath}`) // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceInstallResult", success: true, slug: message.mpItem.id, @@ -3276,7 +3280,7 @@ export const webviewMessageHandler = async ( } catch (error) { console.error(`Error installing marketplace item: ${error}`) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceInstallResult", success: false, error: error instanceof Error ? error.message : String(error), @@ -3294,7 +3298,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() // Send success message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: true, slug: message.mpItem.id, @@ -3308,7 +3312,7 @@ export const webviewMessageHandler = async ( ) // Send error message to webview - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: error instanceof Error ? error.message : String(error), @@ -3325,7 +3329,7 @@ export const webviewMessageHandler = async ( vscode.window.showErrorMessage(errorMessage) if (message.mpItem?.id) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "marketplaceRemoveResult", success: false, error: errorMessage, @@ -3441,7 +3445,7 @@ export const webviewMessageHandler = async ( const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { - openFile(command.filePath) + await openFile(command.filePath) } else { vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) } @@ -3571,7 +3575,7 @@ export const webviewMessageHandler = async ( provider.log(`Created new command file: ${filePath}`) // Open the new file in the editor - openFile(filePath) + await openFile(filePath) // Refresh commands list const { getCommands } = await import("../../services/command/commands") @@ -3695,7 +3699,7 @@ export const webviewMessageHandler = async ( const accessToken = await openAiCodexOAuthManager.getAccessToken() if (!accessToken) { - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", error: "Not authenticated with OpenAI Codex", }) @@ -3706,14 +3710,14 @@ export const webviewMessageHandler = async ( const { fetchOpenAiCodexRateLimitInfo } = await import("../../integrations/openai-codex/rate-limits") const rateLimits = await fetchOpenAiCodexRateLimitInfo(accessToken, { accountId }) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", values: rateLimits, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) provider.log(`Error fetching OpenAI Codex rate limits: ${errorMessage}`) - provider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "openAiCodexRateLimits", error: errorMessage, }) @@ -3839,7 +3843,7 @@ export const webviewMessageHandler = async ( createNewBranch: message.worktreeCreateNewBranch, }, (progress) => { - provider.postMessageToWebview({ + void provider.postMessageToWebview({ type: "worktreeCopyProgress", copyProgressBytesCopied: progress.bytesCopied, copyProgressItemName: progress.itemName, diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index cea212fefa..65965eb8d5 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -34,7 +34,7 @@ export default [ { // Ratchet: enforce no-floating-promises directory by directory. Each // directory is added here once its floating promises are resolved. - files: ["activate/**/*.ts", "core/task/**/*.ts"], + files: ["activate/**/*.ts", "core/task/**/*.ts", "core/webview/**/*.ts"], languageOptions: { parserOptions: { project: true,