mirror of
https://github.com/cline/cline.git
synced 2026-10-29 17:42:34 +08:00
chore: migrate host logging to shared Logger service (#8820)
* chore: migrate host logging to shared Logger service - Replace HostProvider.logToChannel usage with Logger.log/error in controller, webview, and checkpoint migration code - Remove redundant, low-value log statements from Cline API methods to reduce noise - Centralize logging through shared Logger service for more consistent, structured logging and easier maintenance - Remove redundant , low-value log statements from StateManager where we logged error that would be throw and get logged again * Update src/integrations/checkpoints/CheckpointMigration.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix * update tests * update tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -25,7 +25,7 @@ async function handleInstallWithCline(
|
||||
await controller.postStateToWebview()
|
||||
await controller.initTask(installTask)
|
||||
|
||||
HostProvider.get().logToChannel(`Started task to install ${dependencyName}`)
|
||||
Logger.log(`[handleInstallWithCline] Started task to install ${dependencyName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,7 +119,6 @@ export class Controller {
|
||||
|
||||
constructor(readonly context: vscode.ExtensionContext) {
|
||||
PromptRegistry.getInstance() // Ensure prompts and tools are registered
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.stateManager = StateManager.get()
|
||||
StateManager.get().registerCallbacks({
|
||||
onPersistenceError: async ({ error }: PersistenceErrorEvent) => {
|
||||
@@ -154,6 +153,8 @@ export class Controller {
|
||||
|
||||
// Check CLI installation status once on startup
|
||||
checkCliInstallation(this)
|
||||
|
||||
Logger.log("[Controller] ClineProvider instantiated")
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
}
|
||||
|
||||
await fs.writeFile(vercelAiGatewayModelsFilePath, JSON.stringify(models))
|
||||
Logger.log("Vercel AI Gateway models fetched and saved", JSON.stringify(models).slice(0, 300))
|
||||
Logger.log("Vercel AI Gateway models fetched and saved")
|
||||
} else {
|
||||
Logger.error("Invalid response from Vercel AI Gateway API")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isClineCliInstalled } from "@/utils/cli-detector"
|
||||
import { Controller } from ".."
|
||||
|
||||
@@ -12,8 +11,7 @@ export async function checkCliInstallation(_controller: Controller): Promise<Boo
|
||||
try {
|
||||
const isInstalled = await isClineCliInstalled()
|
||||
return Boolean.create({ value: isInstalled })
|
||||
} catch (error) {
|
||||
Logger.error("Failed to check CLI installation:", error)
|
||||
} catch {
|
||||
return Boolean.create({ value: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
} from "@shared/storage/state-keys"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import {
|
||||
getTaskHistoryStateFilePath,
|
||||
@@ -267,12 +265,7 @@ export class StateManager {
|
||||
Object.assign(this.taskStateCache, taskSettings)
|
||||
} catch (error) {
|
||||
// If reading fails, just use empty cache
|
||||
|
||||
Logger.error("[StateManager] Failed to load task settings:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to load task settings, defaulting to globally selected settings.`,
|
||||
})
|
||||
Logger.error("[StateManager] Failed to load task settings, defaulting to globally selected settings.", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +728,6 @@ export class StateManager {
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
Logger.error("[StateManager] Failed to persist global state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -765,7 +757,6 @@ export class StateManager {
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
Logger.error("[StateManager] Failed to persist task settings batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -786,7 +777,6 @@ export class StateManager {
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
Logger.error("Failed to persist secrets batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -803,7 +793,6 @@ export class StateManager {
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
Logger.error("Failed to persist workspace state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
+4
-13
@@ -1,48 +1,39 @@
|
||||
import { Controller } from "@core/controller"
|
||||
import { sendChatButtonClickedEvent } from "@core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineAPI } from "./cline"
|
||||
|
||||
export function createClineAPI(sidebarController: Controller): ClineAPI {
|
||||
const api: ClineAPI = {
|
||||
startNewTask: async (task?: string, images?: string[]) => {
|
||||
HostProvider.get().logToChannel("Starting new task")
|
||||
await sidebarController.clearTask()
|
||||
await sidebarController.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent()
|
||||
await sidebarController.initTask(task, images)
|
||||
HostProvider.get().logToChannel(
|
||||
`Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)`,
|
||||
)
|
||||
},
|
||||
|
||||
sendMessage: async (message?: string, images?: string[]) => {
|
||||
HostProvider.get().logToChannel(
|
||||
`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
|
||||
)
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("messageResponse", message || "", images || [])
|
||||
} else {
|
||||
HostProvider.get().logToChannel("No active task to send message to")
|
||||
Logger.error("No active task to send message to")
|
||||
}
|
||||
},
|
||||
|
||||
pressPrimaryButton: async () => {
|
||||
HostProvider.get().logToChannel("Pressing primary button")
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("yesButtonClicked", "", [])
|
||||
} else {
|
||||
HostProvider.get().logToChannel("No active task to press button for")
|
||||
Logger.error("No active task to press button for")
|
||||
}
|
||||
},
|
||||
|
||||
pressSecondaryButton: async () => {
|
||||
HostProvider.get().logToChannel("Pressing secondary button")
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("noButtonClicked", "", [])
|
||||
} else {
|
||||
HostProvider.get().logToChannel("No active task to press button for")
|
||||
Logger.error("No active task to press button for")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
// if the extension is starting a new session, clear previous task state
|
||||
this.controller.clearTask()
|
||||
|
||||
HostProvider.get().logToChannel("Webview view resolved")
|
||||
Logger.log("[VscodeWebviewProvider] Webview view resolved")
|
||||
|
||||
// Title setting logic removed to allow VSCode to use the container title primarily.
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import { Logger } from "@/shared/services/Logger"
|
||||
*/
|
||||
export async function cleanupLegacyCheckpoints(): Promise<void> {
|
||||
try {
|
||||
HostProvider.get().logToChannel("Checking for legacy checkpoints...")
|
||||
|
||||
const tasksDir = path.join(HostProvider.get().globalStorageFsPath, "tasks")
|
||||
|
||||
// Check if tasks directory exists
|
||||
@@ -45,29 +43,27 @@ export async function cleanupLegacyCheckpoints(): Promise<void> {
|
||||
const checkpointsDir = path.join(mostRecentFolder.path, "checkpoints")
|
||||
|
||||
if (await fileExistsAtPath(checkpointsDir)) {
|
||||
HostProvider.get().logToChannel("Found legacy checkpoints directory, cleaning up...")
|
||||
|
||||
const results = { deleted: [] as string[], failed: [] as string[] }
|
||||
// Legacy checkpoints found, delete checkpoints directories in all task folders
|
||||
for (const folder of folderStats) {
|
||||
const folderCheckpointsDir = path.join(folder.path, "checkpoints")
|
||||
if (await fileExistsAtPath(folderCheckpointsDir)) {
|
||||
HostProvider.get().logToChannel(`Deleting legacy checkpoints in ${folder.folder}`)
|
||||
try {
|
||||
await fs.rm(folderCheckpointsDir, { recursive: true, force: true })
|
||||
results.deleted.push(folder.folder)
|
||||
} catch (_error) {
|
||||
// Ignore error if directory removal fails
|
||||
HostProvider.get().logToChannel(
|
||||
`Warning: Failed to delete checkpoints in ${folder.folder}, continuing...`,
|
||||
)
|
||||
results.failed.push(folder.folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HostProvider.get().logToChannel("Legacy checkpoints cleanup completed")
|
||||
Logger.info(
|
||||
`Legacy checkpoints cleanup completed. Deleted: ${results.deleted.length}, Failed: ${results.failed.length}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
HostProvider.get().logToChannel(`Error cleaning up legacy checkpoints: ${error}`)
|
||||
Logger.error("Error cleaning up legacy checkpoints:", error)
|
||||
throw new Error("Error cleaning up legacy checkpoints.", { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function initializeDistinctId(context: ExtensionContext, uuid: () =
|
||||
|
||||
setDistinctId(distinctId)
|
||||
|
||||
Logger.log("Telemetry distinct ID initialized:", distinctId)
|
||||
Logger.log("[DistinctId] initialized:", distinctId)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,7 +46,7 @@ async function getMachineId(): Promise<string | undefined> {
|
||||
const id = await machineId()
|
||||
return id
|
||||
} catch (error) {
|
||||
Logger.log("Failed to get machine ID from node-machine-id", error)
|
||||
Logger.log("[DistinctId] Failed to get machine ID from node-machine-id", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ async function getMachineId(): Promise<string | undefined> {
|
||||
*/
|
||||
export function setDistinctId(newId: string) {
|
||||
if (_distinctId && _distinctId !== newId) {
|
||||
Logger.log(`Changing telemetry ID from ${_distinctId} to ${newId}.`)
|
||||
Logger.log("[DistinctId] Updating...", `From ${_distinctId} to ${newId}`)
|
||||
}
|
||||
_distinctId = newId
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export function setDistinctId(newId: string) {
|
||||
*/
|
||||
export function getDistinctId() {
|
||||
if (!_distinctId) {
|
||||
Logger.error("Telemetry ID is not initialized. Call initializeDistinctId() first.")
|
||||
Logger.debug("[DistinctId] Not initialized. Call initializeDistinctId() first.")
|
||||
}
|
||||
return _distinctId
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class ClineBlobStorage extends ClineStorage {
|
||||
this.adapter = adapter
|
||||
this.settings = settings
|
||||
this.initialized = true
|
||||
Logger.log("[ClineBlobStorage] Adapter created")
|
||||
Logger.log(`[ClineBlobStorage] Adapter created for ${settings.adapterType}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// Log but don't throw - allow startup to continue
|
||||
@@ -114,8 +114,8 @@ export class ClineBlobStorage extends ClineStorage {
|
||||
}
|
||||
try {
|
||||
return await this.adapter!.read(key)
|
||||
} catch (error) {
|
||||
Logger.error(`[ClineBlobStorage] failed to get '${key}':`, error)
|
||||
} catch {
|
||||
// Silently return undefined on read errors
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,6 @@ export class ClineBlobStorage extends ClineStorage {
|
||||
try {
|
||||
await this.adapter!.write(key, value)
|
||||
} catch (error) {
|
||||
Logger.error(`[ClineBlobStorage] failed to store '${key}':`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -141,7 +140,6 @@ export class ClineBlobStorage extends ClineStorage {
|
||||
try {
|
||||
await this.adapter!.remove(key)
|
||||
} catch (error) {
|
||||
Logger.error(`[ClineBlobStorage] failed to delete '${key}':`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ export class ClineSecretStorage extends ClineStorage {
|
||||
protected async _get(key: string): Promise<string | undefined> {
|
||||
try {
|
||||
return key ? await this.storage.get(key) : undefined
|
||||
} catch (error) {
|
||||
Logger.error("[ClineSecretStorage]", error)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -51,12 +50,11 @@ export class ClineSecretStorage extends ClineStorage {
|
||||
await this.storage.store(key, value)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("[ClineSecretStorage]", error)
|
||||
Logger.error("[ClineSecretStorage] Failed to store", error)
|
||||
}
|
||||
}
|
||||
|
||||
protected async _delete(key: string): Promise<void> {
|
||||
Logger.info("[ClineSecretStorage] deleting secret")
|
||||
await this.storage.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ export abstract class ClineStorage {
|
||||
public async get(key: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await this._get(key)
|
||||
} catch (error) {
|
||||
Logger.error(`[${this.name}] failed to get '${key}':`, error)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -77,8 +76,8 @@ export abstract class ClineStorage {
|
||||
try {
|
||||
await this._delete(key)
|
||||
await this.fire(key)
|
||||
} catch (error) {
|
||||
Logger.error(`[${this.name}] failed to delete '${key}':`, error)
|
||||
} catch {
|
||||
// Silently fail on delete errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,23 @@ import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as should from "should"
|
||||
import * as sinon from "sinon"
|
||||
import { createClineAPI } from "@/exports"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ClineAPI } from "../exports/cline"
|
||||
import { setVscodeHostProviderMock } from "./host-provider-test-utils"
|
||||
|
||||
describe("ClineAPI Core Functionality", () => {
|
||||
let api: ClineAPI
|
||||
let mockController: any
|
||||
let mockLogToChannel: sinon.SinonStub<[string], void>
|
||||
let mockLoggerError: sinon.SinonStub
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let _getGlobalStateStub: sinon.SinonStub
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock log function
|
||||
mockLogToChannel = sandbox.stub<[string], void>()
|
||||
setVscodeHostProviderMock({ logToChannel: mockLogToChannel })
|
||||
// Stub Logger.error
|
||||
mockLoggerError = sandbox.stub(Logger, "error")
|
||||
setVscodeHostProviderMock({})
|
||||
|
||||
// Create a mock controller that matches what the real createClineAPI expects
|
||||
// We don't import the real Controller to avoid the webview dependencies
|
||||
@@ -64,11 +65,6 @@ describe("ClineAPI Core Functionality", () => {
|
||||
sinon.assert.called(mockController.clearTask)
|
||||
sinon.assert.called(mockController.postStateToWebview)
|
||||
sinon.assert.calledWith(mockController.initTask, taskDescription, images)
|
||||
|
||||
// Verify logging - first it logs "Starting new task"
|
||||
sinon.assert.calledWith(mockLogToChannel, "Starting new task")
|
||||
// Then it logs the task details
|
||||
sinon.assert.calledWith(mockLogToChannel, `Task started with message: "Create a test function" and 2 image(s)`)
|
||||
})
|
||||
|
||||
it("should handle undefined task description", async () => {
|
||||
@@ -76,16 +72,12 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.called(mockController.clearTask)
|
||||
sinon.assert.calledWith(mockController.initTask, undefined, [])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "Task started with message: undefined and 0 image(s)")
|
||||
})
|
||||
|
||||
it("should handle task with no images", async () => {
|
||||
await api.startNewTask("Task without images")
|
||||
|
||||
sinon.assert.calledWith(mockController.initTask, "Task without images", undefined)
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, `Task started with message: "Task without images" and 0 image(s)`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,16 +91,12 @@ describe("ClineAPI Core Functionality", () => {
|
||||
await api.sendMessage("Test message", ["image.png"])
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "messageResponse", "Test message", ["image.png"])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, `Sending message: "Test message" with 1 image(s)`)
|
||||
})
|
||||
|
||||
it("should handle no active task gracefully", async () => {
|
||||
mockController.task = undefined
|
||||
|
||||
await api.sendMessage("Message to nowhere", [])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to send message to")
|
||||
})
|
||||
|
||||
it("should handle empty message", async () => {
|
||||
@@ -131,8 +119,6 @@ describe("ClineAPI Core Functionality", () => {
|
||||
await api.sendMessage(undefined, [])
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "messageResponse", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, `Sending message: undefined with 0 image(s)`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -147,8 +133,6 @@ describe("ClineAPI Core Functionality", () => {
|
||||
await api.pressPrimaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "yesButtonClicked", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "Pressing primary button")
|
||||
})
|
||||
|
||||
it("should handle primary button press with no active task", async () => {
|
||||
@@ -156,7 +140,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
await api.pressPrimaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to press button for")
|
||||
sinon.assert.calledWith(mockLoggerError, "No active task to press button for")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,8 +154,6 @@ describe("ClineAPI Core Functionality", () => {
|
||||
await api.pressSecondaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "noButtonClicked", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "Pressing secondary button")
|
||||
})
|
||||
|
||||
it("should handle secondary button press with no active task", async () => {
|
||||
@@ -179,7 +161,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
await api.pressSecondaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to press button for")
|
||||
sinon.assert.calledWith(mockLoggerError, "No active task to press button for")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user