mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ff09d7550 | |||
| 0d04205dc4 | |||
| 4b9dbf11a0 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: preserve file endings and trailing newlines across all edit tools
|
||||
@@ -18,6 +18,48 @@ This file is the secret sauce for working effectively in this codebase. It captu
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
|
||||
|
||||
## Debug Logging (For Development/Debugging Only)
|
||||
**IMPORTANT: These utilities are for debugging purposes only, not production logging.**
|
||||
|
||||
The codebase has a unified debug logging system that writes both extension and webview logs to a single file: `~/cline-debug.log`. This makes it easy to troubleshoot issues that span the extension/webview boundary.
|
||||
|
||||
**Files:**
|
||||
- `src/utils/debugLogger.ts` - Extension-side logger with file writing
|
||||
- `webview-ui/src/utils/webviewDebugLogger.ts` - Webview-side logger that intercepts console calls and sends to extension
|
||||
|
||||
**Extension usage:**
|
||||
```typescript
|
||||
import { extensionLog } from '@/utils/debugLogger'
|
||||
extensionLog.info('Task started:', taskId)
|
||||
extensionLog.error('Failed to load:', error)
|
||||
```
|
||||
|
||||
**Webview usage:**
|
||||
```typescript
|
||||
// In your app entry point (e.g., App.tsx or index.tsx):
|
||||
import { enableWebviewDebugLogging } from './utils/webviewDebugLogger'
|
||||
enableWebviewDebugLogging()
|
||||
|
||||
// Then use console normally - it gets logged to file:
|
||||
console.log('Button clicked:', buttonId)
|
||||
console.error('API failed:', error)
|
||||
```
|
||||
|
||||
**Monitoring logs in real-time:**
|
||||
```bash
|
||||
tail -f ~/cline-debug.log
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
1. Extension logs directly write to `~/cline-debug.log` using Node.js fs
|
||||
2. Webview logs intercept console calls, stringify args, and send to extension via `postMessage` with type `webview_debug_log`
|
||||
3. Extension receives `webview_debug_log` messages and appends them to the same file
|
||||
4. Logs are formatted with timestamp, source (EXTENSION/WEBVIEW), level, and message
|
||||
5. Automatic log rotation when file exceeds 10MB
|
||||
|
||||
**When to add webview message handler:**
|
||||
If you're integrating the webview logger, you need to handle the `webview_debug_log` message type in the extension's message handler. Look for where other webview messages are processed and add handling for this type that calls `debugLog('webview', level, ...args)`.
|
||||
|
||||
## gRPC/Protobuf Communication
|
||||
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
changes[path] = {
|
||||
type: PatchActionType.UPDATE,
|
||||
oldContent: originalFiles[path],
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path).trimEnd(),
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path),
|
||||
movePath: action.movePath,
|
||||
}
|
||||
break
|
||||
|
||||
@@ -480,8 +480,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
return
|
||||
}
|
||||
|
||||
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
|
||||
|
||||
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
|
||||
}
|
||||
|
||||
|
||||
@@ -96,17 +96,34 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Replace the text in the diff editor document.
|
||||
const document = this.activeDiffEditor?.document
|
||||
const document = this.activeDiffEditor.document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
|
||||
edit.replace(document.uri, range, content)
|
||||
|
||||
// IMPORTANT: VS Code may treat an out-of-bounds end position as an insertion instead of a
|
||||
// replacement. Always validate the range against the current document to keep edits
|
||||
// strictly within the real end-of-file.
|
||||
const startLine = Math.max(0, Math.min(rangeToReplace.startLine, document.lineCount - 1))
|
||||
const desiredEndLine = Math.max(rangeToReplace.startLine, rangeToReplace.endLine)
|
||||
const validatedRange = document.validateRange(
|
||||
new vscode.Range(new vscode.Position(startLine, 0), new vscode.Position(desiredEndLine, 0)),
|
||||
)
|
||||
|
||||
edit.replace(document.uri, validatedRange, content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Preserve trailing newline: if content ends with newline, ensure document does too
|
||||
if (content.endsWith("\n") && !document.getText().endsWith("\n")) {
|
||||
const fixEdit = new vscode.WorkspaceEdit()
|
||||
fixEdit.insert(document.uri, document.lineAt(Math.max(0, document.lineCount - 1)).range.end, "\n")
|
||||
await vscode.workspace.applyEdit(fixEdit)
|
||||
}
|
||||
|
||||
if (currentLine !== undefined) {
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
|
||||
@@ -182,7 +182,12 @@ export abstract class DiffViewProvider {
|
||||
// Replace all content up to the current line with accumulated lines
|
||||
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags
|
||||
// on previous lines are auto closed for example
|
||||
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||
let contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n")
|
||||
if (!isFinal) {
|
||||
// During streaming, add trailing newline for cursor positioning
|
||||
contentToReplace += "\n"
|
||||
}
|
||||
|
||||
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
|
||||
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
|
||||
|
||||
@@ -212,15 +217,6 @@ export abstract class DiffViewProvider {
|
||||
if (isFinal) {
|
||||
// Handle any remaining lines if the new content is shorter than the original
|
||||
await this.truncateDocument(this.streamedLines.length)
|
||||
|
||||
// Add empty last line if original content had one
|
||||
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
|
||||
if (hasEmptyLastLine) {
|
||||
const accumulatedLines = accumulatedContent.split("\n")
|
||||
if (accumulatedLines[accumulatedLines.length - 1] !== "") {
|
||||
accumulatedContent += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,10 +273,10 @@ export abstract class DiffViewProvider {
|
||||
|
||||
// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
|
||||
const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
|
||||
const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
|
||||
const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // this is the final content we return to the model to use as the new baseline for future edits
|
||||
const normalizedPreSaveContent = preSaveContent.replace(/\r\n|\n/g, newContentEOL)
|
||||
const normalizedPostSaveContent = postSaveContent.replace(/\r\n|\n/g, newContentEOL) // this is the final content we return to the model to use as the new baseline for future edits
|
||||
// just in case the new content has a mix of varying EOL characters
|
||||
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
|
||||
const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL)
|
||||
|
||||
let userEdits: string | undefined
|
||||
if (normalizedPreSaveContent !== normalizedNewContent) {
|
||||
|
||||
@@ -43,19 +43,48 @@ export class FileEditProvider extends DiffViewProvider {
|
||||
|
||||
// Split the document into lines
|
||||
const lines = this.documentContent.split("\n")
|
||||
const originalEndsWithNewline = this.documentContent.endsWith("\n")
|
||||
|
||||
// If original ends with newline, split creates a trailing empty string that isn't a real line.
|
||||
// Remove it for line-based operations, we'll add it back at the end if needed.
|
||||
const realLines = originalEndsWithNewline && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines
|
||||
|
||||
// Replace the specified range with the new content
|
||||
const newContentLines = content.split("\n")
|
||||
// Remove trailing empty line if present in newContentLines for proper splicing
|
||||
if (newContentLines[newContentLines.length - 1] === "") {
|
||||
const contentEndsWithNewline = content.endsWith("\n")
|
||||
|
||||
// Determine if we're replacing to the end of the document
|
||||
const replacingToEnd = rangeToReplace.endLine >= realLines.length
|
||||
|
||||
// Handle trailing empty string from split:
|
||||
// - If content ends with \n, split creates an empty string at the end
|
||||
// - When replacing to end: this empty string becomes the document's trailing newline - keep it
|
||||
// - When replacing middle: this empty string would create an extra newline - remove it
|
||||
// (the join operation will naturally add newlines between lines)
|
||||
// - If content doesn't end with \n but split created empty string, remove it
|
||||
if (!contentEndsWithNewline && newContentLines[newContentLines.length - 1] === "") {
|
||||
newContentLines.pop()
|
||||
} else if (contentEndsWithNewline && !replacingToEnd && newContentLines[newContentLines.length - 1] === "") {
|
||||
// Content ends with newline but we're replacing middle section - remove trailing empty string
|
||||
newContentLines.pop()
|
||||
}
|
||||
|
||||
// Splice the lines array to replace the range
|
||||
lines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newContentLines)
|
||||
// Splice the real lines array to replace the range
|
||||
realLines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newContentLines)
|
||||
|
||||
// Join the lines back together
|
||||
this.documentContent = lines.join("\n")
|
||||
let result = realLines.join("\n")
|
||||
|
||||
// Preserve trailing newline: add it back if original had one OR if we replaced to end with content that ends with newline
|
||||
const shouldHaveTrailingNewline = originalEndsWithNewline || (replacingToEnd && contentEndsWithNewline)
|
||||
if (shouldHaveTrailingNewline && !result.endsWith("\n")) {
|
||||
result += "\n"
|
||||
} else if (!shouldHaveTrailingNewline && result.endsWith("\n")) {
|
||||
// Shouldn't have trailing newline but result has one - remove it
|
||||
result = result.slice(0, -1)
|
||||
}
|
||||
|
||||
this.documentContent = result
|
||||
}
|
||||
|
||||
protected async scrollEditorToLine(_line: number): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as assert from "assert"
|
||||
import { describe, it } from "mocha"
|
||||
import { FileEditProvider } from "../integrations/editor/FileEditProvider"
|
||||
|
||||
describe("FileEditProvider Trailing Newline", () => {
|
||||
// Helper to set up provider without calling open()
|
||||
function setupProvider(initialContent: string): FileEditProvider {
|
||||
const provider = new FileEditProvider()
|
||||
provider["isEditing"] = true
|
||||
provider["documentContent"] = initialContent
|
||||
provider["originalContent"] = initialContent
|
||||
return provider
|
||||
}
|
||||
|
||||
it("preserves trailing newline when content ends with newline", async () => {
|
||||
const provider = setupProvider("line1\nline2\n")
|
||||
|
||||
await provider.replaceText("new1\nnew2\n", { startLine: 0, endLine: 2 }, undefined)
|
||||
const result = await provider.getContent()
|
||||
|
||||
assert.strictEqual(result, "new1\nnew2\n")
|
||||
assert.strictEqual(result?.endsWith("\n"), true)
|
||||
})
|
||||
|
||||
it("does not add trailing newline when content does not end with newline", async () => {
|
||||
const provider = setupProvider("line1\nline2")
|
||||
|
||||
await provider.replaceText("new1\nnew2", { startLine: 0, endLine: 2 }, undefined)
|
||||
const result = await provider.getContent()
|
||||
|
||||
assert.strictEqual(result, "new1\nnew2")
|
||||
assert.strictEqual(result?.endsWith("\n"), false)
|
||||
})
|
||||
|
||||
it("preserves trailing newline when replacing middle section", async () => {
|
||||
const provider = setupProvider("line1\nline2\nline3\n")
|
||||
|
||||
await provider.replaceText("new2\n", { startLine: 1, endLine: 2 }, undefined)
|
||||
const result = await provider.getContent()
|
||||
|
||||
assert.strictEqual(result, "line1\nnew2\nline3\n")
|
||||
assert.strictEqual(result?.endsWith("\n"), true)
|
||||
})
|
||||
|
||||
it("handles file without trailing newline correctly", async () => {
|
||||
const provider = setupProvider("line1\nline2")
|
||||
|
||||
await provider.replaceText("new1\nnew2\n", { startLine: 0, endLine: 2 }, undefined)
|
||||
const result = await provider.getContent()
|
||||
|
||||
assert.strictEqual(result, "new1\nnew2\n")
|
||||
assert.strictEqual(result?.endsWith("\n"), true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as assert from "assert"
|
||||
import { describe, it } from "mocha"
|
||||
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
|
||||
|
||||
class TestDiffViewProvider extends DiffViewProvider {
|
||||
public documentText: string = ""
|
||||
public replacements: { content: string; range: { startLine: number; endLine: number } }[] = []
|
||||
|
||||
async openDiffEditor(): Promise<void> {}
|
||||
async scrollEditorToLine(line: number): Promise<void> {}
|
||||
async scrollAnimation(startLine: number, endLine: number): Promise<void> {}
|
||||
async truncateDocument(lineNumber: number): Promise<void> {
|
||||
const lines = this.documentText.split("\n")
|
||||
this.documentText = lines.slice(0, lineNumber).join("\n")
|
||||
}
|
||||
async getDocumentText(): Promise<string | undefined> {
|
||||
return this.documentText
|
||||
}
|
||||
async saveDocument(): Promise<Boolean> {
|
||||
return true
|
||||
}
|
||||
async closeAllDiffViews(): Promise<void> {}
|
||||
async resetDiffView(): Promise<void> {}
|
||||
|
||||
async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number | undefined,
|
||||
): Promise<void> {
|
||||
this.replacements.push({ content, range: rangeToReplace })
|
||||
// Simulate the replacement
|
||||
const lines = this.documentText.split("\n")
|
||||
const newLines = content.split("\n")
|
||||
// Preserve trailing newline logic (simplified)
|
||||
if (!content.endsWith("\n") && newLines[newLines.length - 1] === "") {
|
||||
newLines.pop()
|
||||
}
|
||||
lines.splice(rangeToReplace.startLine, rangeToReplace.endLine - rangeToReplace.startLine, ...newLines)
|
||||
this.documentText = lines.join("\n")
|
||||
}
|
||||
|
||||
public setup(initialContent: string) {
|
||||
this.isEditing = true
|
||||
this.documentText = initialContent
|
||||
this.originalContent = initialContent
|
||||
}
|
||||
}
|
||||
|
||||
describe("DiffViewProvider Newline handling", () => {
|
||||
it("preserves trailing newline through update() when content ends with newline", async () => {
|
||||
const provider = new TestDiffViewProvider()
|
||||
provider.setup("line1\nline2\n")
|
||||
|
||||
await provider.update("new1\nnew2\n", true)
|
||||
const result = await provider.getDocumentText()
|
||||
|
||||
assert.strictEqual(result, "new1\nnew2\n")
|
||||
assert.strictEqual(result?.endsWith("\n"), true)
|
||||
})
|
||||
|
||||
it("does not add trailing newline when content does not end with newline", async () => {
|
||||
const provider = new TestDiffViewProvider()
|
||||
provider.setup("line1\nline2")
|
||||
|
||||
await provider.update("new1\nnew2", true)
|
||||
const result = await provider.getDocumentText()
|
||||
|
||||
assert.strictEqual(result, "new1\nnew2")
|
||||
assert.strictEqual(result?.endsWith("\n"), false)
|
||||
})
|
||||
|
||||
it("handles file without trailing newline correctly", async () => {
|
||||
const provider = new TestDiffViewProvider()
|
||||
provider.setup("[6]: http://chris.beams.io/posts/git-commit/#seven-rules")
|
||||
|
||||
await provider.update("new content\n", true)
|
||||
const result = await provider.getDocumentText()
|
||||
|
||||
assert.strictEqual(result, "new content\n")
|
||||
assert.strictEqual(result?.endsWith("\n"), true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Unified Debug Logger
|
||||
*
|
||||
* **IMPORTANT: This is for debugging purposes only.**
|
||||
*
|
||||
* Provides logging utilities for both extension and webview to help troubleshoot issues.
|
||||
* All logs are written to ~/cline-debug.log for easy access during development and debugging.
|
||||
*
|
||||
* This logger is NOT for production logging - it's specifically designed to help developers
|
||||
* diagnose problems during development, testing, or when users report issues.
|
||||
*
|
||||
* Usage:
|
||||
* import { debugLog, extensionLog } from '@/utils/debugLogger'
|
||||
* extensionLog.info('Task started:', taskId)
|
||||
* debugLog('extension', 'warn', 'Something unexpected happened')
|
||||
*
|
||||
* Monitoring logs in real-time:
|
||||
* tail -f ~/cline-debug.log
|
||||
*/
|
||||
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
// Single unified log file for both extension and webview
|
||||
export const DEBUG_LOG_PATH = path.join(os.homedir(), "cline-debug.log")
|
||||
|
||||
// Max log file size before rotation (10MB)
|
||||
const MAX_LOG_SIZE = 10 * 1024 * 1024
|
||||
|
||||
/**
|
||||
* Log levels
|
||||
*/
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error"
|
||||
|
||||
/**
|
||||
* Log source (extension or webview)
|
||||
*/
|
||||
export type LogSource = "extension" | "webview"
|
||||
|
||||
/**
|
||||
* Format a log entry with timestamp, source, and level
|
||||
*/
|
||||
function formatLogEntry(source: LogSource, level: LogLevel, args: unknown[]): string {
|
||||
const timestamp = new Date().toISOString()
|
||||
const levelUpper = level.toUpperCase().padEnd(5) // Align columns
|
||||
const sourceLabel = `[${source.toUpperCase()}]`.padEnd(11) // Align columns
|
||||
|
||||
// Convert args to strings
|
||||
const message = args
|
||||
.map((arg) => {
|
||||
if (arg === undefined) return "undefined"
|
||||
if (arg === null) return "null"
|
||||
if (typeof arg === "string") return arg
|
||||
if (typeof arg === "number" || typeof arg === "boolean") return String(arg)
|
||||
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2)
|
||||
} catch {
|
||||
return String(arg)
|
||||
}
|
||||
})
|
||||
.join(" ")
|
||||
|
||||
return `[${timestamp}] ${sourceLabel} [${levelUpper}] ${message}\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if log file needs rotation and rotate if necessary
|
||||
*/
|
||||
function rotateLogIfNeeded(): void {
|
||||
try {
|
||||
if (fs.existsSync(DEBUG_LOG_PATH)) {
|
||||
const stats = fs.statSync(DEBUG_LOG_PATH)
|
||||
if (stats.size > MAX_LOG_SIZE) {
|
||||
const rotatedPath = `${DEBUG_LOG_PATH}.old`
|
||||
// Remove old backup if it exists
|
||||
if (fs.existsSync(rotatedPath)) {
|
||||
fs.unlinkSync(rotatedPath)
|
||||
}
|
||||
// Rotate current log to backup
|
||||
fs.renameSync(DEBUG_LOG_PATH, rotatedPath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - don't let logging errors break the app
|
||||
console.error("[debugLogger] Failed to rotate log:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry to the debug log file
|
||||
*
|
||||
* @param source - Where the log is coming from (extension or webview)
|
||||
* @param level - Log level (debug, info, warn, error)
|
||||
* @param args - Arguments to log (will be stringified)
|
||||
*/
|
||||
export function debugLog(source: LogSource, level: LogLevel, ...args: unknown[]): void {
|
||||
try {
|
||||
// Check if rotation is needed
|
||||
rotateLogIfNeeded()
|
||||
|
||||
// Format and append log entry
|
||||
const logEntry = formatLogEntry(source, level, args)
|
||||
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, "utf8")
|
||||
} catch (error) {
|
||||
// Silently fail - don't let logging errors break the app
|
||||
console.error("[debugLogger] Failed to write log:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of debugLog for non-blocking logging
|
||||
* Recommended for high-frequency logging scenarios
|
||||
*/
|
||||
export async function debugLogAsync(source: LogSource, level: LogLevel, ...args: unknown[]): Promise<void> {
|
||||
try {
|
||||
// Check if rotation is needed
|
||||
rotateLogIfNeeded()
|
||||
|
||||
// Format and append log entry
|
||||
const logEntry = formatLogEntry(source, level, args)
|
||||
await fs.promises.appendFile(DEBUG_LOG_PATH, logEntry, "utf8")
|
||||
} catch (error) {
|
||||
// Silently fail - don't let logging errors break the app
|
||||
console.error("[debugLogger] Failed to write log:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrappers for extension logging
|
||||
*
|
||||
* Use these for debugging extension-side code:
|
||||
* - extensionLog.debug() - Verbose debugging info
|
||||
* - extensionLog.info() - General informational messages
|
||||
* - extensionLog.warn() - Warnings about potential issues
|
||||
* - extensionLog.error() - Error conditions
|
||||
*/
|
||||
export const extensionLog = {
|
||||
debug: (...args: unknown[]) => debugLog("extension", "debug", ...args),
|
||||
info: (...args: unknown[]) => debugLog("extension", "info", ...args),
|
||||
warn: (...args: unknown[]) => debugLog("extension", "warn", ...args),
|
||||
error: (...args: unknown[]) => debugLog("extension", "error", ...args),
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrappers for webview logging
|
||||
*
|
||||
* Use these for debugging webview-side code:
|
||||
* - webviewLog.debug() - Verbose debugging info
|
||||
* - webviewLog.info() - General informational messages
|
||||
* - webviewLog.warn() - Warnings about potential issues
|
||||
* - webviewLog.error() - Error conditions
|
||||
*/
|
||||
export const webviewLog = {
|
||||
debug: (...args: unknown[]) => debugLog("webview", "debug", ...args),
|
||||
info: (...args: unknown[]) => debugLog("webview", "info", ...args),
|
||||
warn: (...args: unknown[]) => debugLog("webview", "warn", ...args),
|
||||
error: (...args: unknown[]) => debugLog("webview", "error", ...args),
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the debug log file
|
||||
*
|
||||
* Useful when starting a fresh debugging session
|
||||
*/
|
||||
export function clearDebugLog(): void {
|
||||
try {
|
||||
if (fs.existsSync(DEBUG_LOG_PATH)) {
|
||||
fs.unlinkSync(DEBUG_LOG_PATH)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[debugLogger] Failed to clear log:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the debug log file path
|
||||
*
|
||||
* Returns: ~/cline-debug.log
|
||||
*/
|
||||
export function getDebugLogPath(): string {
|
||||
return DEBUG_LOG_PATH
|
||||
}
|
||||
Generated
+103
-35
@@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
@@ -171,7 +172,6 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -570,7 +570,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -594,7 +593,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -604,7 +602,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
|
||||
"integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.8.1"
|
||||
}
|
||||
@@ -1126,7 +1123,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz",
|
||||
"integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.18",
|
||||
"@firebase/logger": "0.4.4",
|
||||
@@ -1193,7 +1189,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz",
|
||||
"integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@firebase/app": "0.13.2",
|
||||
"@firebase/component": "0.6.18",
|
||||
@@ -1209,8 +1204,7 @@
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
|
||||
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@firebase/auth-compat": {
|
||||
"version": "0.5.28",
|
||||
@@ -1661,7 +1655,6 @@
|
||||
"integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -2886,7 +2879,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.22.tgz",
|
||||
"integrity": "sha512-+RVuAxjS2QWyLdYTPxv0IfMjhsxa1GKRSwvpii13bOGEQclwwfaNL2MvBbTt1Mzu/LHaX7kyj0THbZnlOplZOA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/react-utils": "2.1.13",
|
||||
"@heroui/system-rsc": "2.3.19",
|
||||
@@ -2981,7 +2973,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.22.tgz",
|
||||
"integrity": "sha512-naKFQBfp7YwhKGmh7rKCC5EBjV7kdozX21fyGHucDYa6GeFfIKVqXILgZ94HZlfp+LGJfV6U+BuKIflevf0Y+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/shared-utils": "2.1.11",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -3764,6 +3755,12 @@
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
@@ -3793,6 +3790,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -3859,6 +3882,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
@@ -4139,6 +4177,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||
@@ -6941,7 +7022,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -7413,7 +7495,6 @@
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -7425,7 +7506,6 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -7898,7 +7978,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -8052,7 +8131,6 @@
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
@@ -8387,7 +8465,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -8788,7 +8865,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -9065,7 +9141,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
@@ -9146,7 +9223,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -9455,7 +9531,6 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.18.tgz",
|
||||
"integrity": "sha512-HBVXBL5x3nk/0WrYM5G4VgjBey99ytVYET5AX17s/pcnlH90cyaxVUqgoN8cpF4+PqZRVOhwWsv28F+hxA9Tzg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.18",
|
||||
"motion-utils": "^12.23.6",
|
||||
@@ -10542,7 +10617,6 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -11036,6 +11110,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -11966,6 +12041,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -11981,6 +12057,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -12053,7 +12130,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -12098,7 +12174,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -12112,7 +12187,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-remark": {
|
||||
"version": "2.1.0",
|
||||
@@ -12634,7 +12710,6 @@
|
||||
"integrity": "sha512-/vFSi3I+ya/D75UZh5GxLc/6UQ+KoKPEvL9autr1yGcaeWzXBQr1tTXmNDS4FImFCPwBAvVe7j9YzR8PQ5rfqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -12980,7 +13055,6 @@
|
||||
"integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -13274,7 +13348,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
@@ -13303,8 +13376,7 @@
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -13619,8 +13691,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
@@ -13628,7 +13699,6 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14041,7 +14111,6 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -14169,7 +14238,6 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import ClineLogoWhite from "@/assets/ClineLogoWhite"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./select"
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Ui/Select",
|
||||
component: Select,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A select dropdown component built on Radix UI. Allows users to choose from a list of options with support for grouping, separators, labels, and custom styling. Includes trigger, content, item, and value components for composing select layouts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type StoryProps = {
|
||||
placeholder: string
|
||||
size: "sm" | "default"
|
||||
items: string[]
|
||||
defaultValue?: string
|
||||
disabled: boolean
|
||||
showGroups: boolean
|
||||
showSeparators: boolean
|
||||
}
|
||||
|
||||
// Interactive story with controls
|
||||
export const Interactive: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
placeholder: "Select an option",
|
||||
size: "default",
|
||||
items: ["Option 1", "Option 2", "Option 3", "Option 4"],
|
||||
defaultValue: undefined,
|
||||
disabled: false,
|
||||
showGroups: false,
|
||||
showSeparators: false,
|
||||
},
|
||||
argTypes: {
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text shown when no option is selected",
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["sm", "default"],
|
||||
description: "Size variant of the select trigger",
|
||||
},
|
||||
items: {
|
||||
control: "object",
|
||||
description: "Array of items to display in the select",
|
||||
},
|
||||
defaultValue: {
|
||||
control: "text",
|
||||
description: "Default selected value",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Disable the select component",
|
||||
},
|
||||
showGroups: {
|
||||
control: "boolean",
|
||||
description: "Show items organized in groups with labels",
|
||||
},
|
||||
showSeparators: {
|
||||
control: "boolean",
|
||||
description: "Show separators between items",
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-full h-full flex justify-center items-center overflow-hidden">
|
||||
<div className="flex flex-col justify-center items-center h-[60%] w-[50%] overflow-hidden mt-50">
|
||||
<div className="flex justify-center my-5">
|
||||
<ClineLogoWhite className="size-16" />
|
||||
</div>
|
||||
<p>
|
||||
You can customize the select using the controls in the "Controls" panel below to change its placeholder, size,
|
||||
items, and styling options.
|
||||
</p>
|
||||
|
||||
<div className="mt-4.5">
|
||||
<Select defaultValue={args.defaultValue} disabled={args.disabled}>
|
||||
<SelectTrigger size={args.size}>
|
||||
<SelectValue placeholder={args.placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
{args.showGroups ? (
|
||||
<>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Group 1</SelectLabel>
|
||||
{args.items.slice(0, Math.ceil(args.items.length / 2)).map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
{args.showSeparators && <SelectSeparator />}
|
||||
<SelectGroup>
|
||||
<SelectLabel>Group 2</SelectLabel>
|
||||
{args.items.slice(Math.ceil(args.items.length / 2)).map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</>
|
||||
) : (
|
||||
args.items.map((item, index) => (
|
||||
<>
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
{args.showSeparators && index < args.items.length - 1 && <SelectSeparator />}
|
||||
</>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
// Showcase all select variants
|
||||
export const Overview = () => {
|
||||
const variants = [
|
||||
{
|
||||
label: "Basic",
|
||||
size: "default" as const,
|
||||
placeholder: "Select a fruit",
|
||||
items: ["Apple", "Banana", "Cherry", "Date", "Elderberry"],
|
||||
hasGroups: false,
|
||||
hasSeparators: false,
|
||||
},
|
||||
{
|
||||
label: "With Groups",
|
||||
size: "default" as const,
|
||||
placeholder: "Select a language",
|
||||
groups: [
|
||||
{
|
||||
label: "Frontend",
|
||||
items: ["JavaScript", "TypeScript", "HTML", "CSS"],
|
||||
},
|
||||
{
|
||||
label: "Backend",
|
||||
items: ["Python", "Java", "Go", "Rust"],
|
||||
},
|
||||
],
|
||||
hasGroups: true,
|
||||
hasSeparators: false,
|
||||
},
|
||||
{
|
||||
label: "With Separators",
|
||||
size: "default" as const,
|
||||
placeholder: "Select a tool",
|
||||
items: ["Git", "Docker", "Kubernetes", "Jenkins"],
|
||||
hasGroups: false,
|
||||
hasSeparators: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="w-screen">
|
||||
<div className="flex justify-center h-[60%] w-[80%] overflow-hidden gap-8 p-8">
|
||||
{variants.map((variant) => (
|
||||
<div className="flex flex-col gap-4" key={variant.label}>
|
||||
<h2 className="text-lg font-semibold">{variant.label}</h2>
|
||||
<Select>
|
||||
<SelectTrigger size={variant.size}>
|
||||
<SelectValue placeholder={variant.placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
{variant.hasGroups && "groups" in variant ? (
|
||||
variant?.groups?.map((group, groupIndex) => (
|
||||
<>
|
||||
<SelectGroup key={group.label}>
|
||||
<SelectLabel>{group.label}</SelectLabel>
|
||||
{group.items.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
{groupIndex < variant.groups.length - 1 && <SelectSeparator />}
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{"items" in variant &&
|
||||
variant?.items?.map((item, index) => (
|
||||
<>
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
{variant.hasSeparators && index < variant.items.length - 1 && (
|
||||
<SelectSeparator />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client"
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"border-editor-group-border data-[placeholder]:text-input-placeholder [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-editor-group-border focus-visible:ring-ring/20 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-error flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-2",
|
||||
className,
|
||||
)}
|
||||
data-size={size}
|
||||
data-slot="select-trigger"
|
||||
{...props}>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
align={align}
|
||||
className={cn(
|
||||
"bg-menu text-menu-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xs border border-editor-group-border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
data-slot="select-content"
|
||||
position={position}
|
||||
{...props}>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
className={cn("text-input-placeholder/80 px-2 py-1 text-sm", className)}
|
||||
data-slot="select-label"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
"focus:bg-button-background/50 focus:text-input [&_svg:not([class*='text-'])]:text-description relative flex w-full cursor-default items-center gap-2 rounded-xs py-1 pr-8 pl-2 text-xs outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-2 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
data-slot="select-item"
|
||||
{...props}>
|
||||
<span className="absolute right-2 flex size-2 items-center justify-center" data-slot="select-item-indicator">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-2" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
className={cn("bg-editor-group-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
data-slot="select-separator"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
data-slot="select-scroll-up-button"
|
||||
{...props}>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
data-slot="select-scroll-down-button"
|
||||
{...props}>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Webview Debug Logger
|
||||
*
|
||||
* **IMPORTANT: This is for debugging purposes only.**
|
||||
*
|
||||
* Intercepts console.log/warn/error/debug calls and sends them to the extension,
|
||||
* which writes them to ~/cline-debug.log (unified with extension logs).
|
||||
*
|
||||
* This allows developers to see webview console output in the same log file as
|
||||
* extension logs, making it easier to debug issues that span both sides of the
|
||||
* extension/webview boundary.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Import and enable early in your app entry point:
|
||||
* import { enableWebviewDebugLogging } from './utils/webviewDebugLogger'
|
||||
* enableWebviewDebugLogging()
|
||||
*
|
||||
* 2. Use console methods normally - they'll be intercepted and logged:
|
||||
* console.log('User clicked button:', buttonId)
|
||||
* console.warn('API request took longer than expected')
|
||||
* console.error('Failed to load data:', error)
|
||||
*
|
||||
* 3. Monitor logs in real-time:
|
||||
* tail -f ~/cline-debug.log
|
||||
*
|
||||
* Note: The extension must handle the 'webview_debug_log' message type for this to work.
|
||||
*/
|
||||
|
||||
import { PLATFORM_CONFIG } from "../config/platform.config"
|
||||
|
||||
// Store original console methods
|
||||
const originalConsole = {
|
||||
log: console.log.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
}
|
||||
|
||||
// Convert any value to a string for logging
|
||||
function stringify(arg: unknown): string {
|
||||
if (arg === undefined) return "undefined"
|
||||
if (arg === null) return "null"
|
||||
if (typeof arg === "string") return arg
|
||||
if (typeof arg === "number" || typeof arg === "boolean") return String(arg)
|
||||
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2)
|
||||
} catch {
|
||||
return String(arg)
|
||||
}
|
||||
}
|
||||
|
||||
// Send log to extension
|
||||
function sendToExtension(level: "log" | "warn" | "error" | "debug", args: unknown[]) {
|
||||
try {
|
||||
PLATFORM_CONFIG.postMessage({
|
||||
type: "webview_debug_log",
|
||||
webview_debug_log: {
|
||||
level,
|
||||
args: args.map(stringify),
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// Silently fail if postMessage isn't available
|
||||
}
|
||||
}
|
||||
|
||||
// Create wrapped console method
|
||||
function createWrapper(level: "log" | "warn" | "error" | "debug") {
|
||||
return (...args: unknown[]) => {
|
||||
// Call original console method
|
||||
originalConsole[level](...args)
|
||||
// Send to extension for file logging
|
||||
sendToExtension(level, args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable webview debug logging.
|
||||
* Call this once at app startup to intercept all console calls.
|
||||
*
|
||||
* After enabling, all console.log/warn/error/debug calls will:
|
||||
* 1. Display in the browser console as normal
|
||||
* 2. Be sent to the extension and written to ~/cline-debug.log
|
||||
*/
|
||||
export function enableWebviewDebugLogging() {
|
||||
console.log = createWrapper("log")
|
||||
console.warn = createWrapper("warn")
|
||||
console.error = createWrapper("error")
|
||||
console.debug = createWrapper("debug")
|
||||
|
||||
// Log that we've enabled debug logging
|
||||
console.log("[WebviewDebugLogger] Debug logging enabled. Logs written to ~/cline-debug.log")
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable webview debug logging and restore original console methods.
|
||||
* Useful if you want to temporarily disable logging or clean up on unmount.
|
||||
*/
|
||||
export function disableWebviewDebugLogging() {
|
||||
console.log = originalConsole.log
|
||||
console.warn = originalConsole.warn
|
||||
console.error = originalConsole.error
|
||||
console.debug = originalConsole.debug
|
||||
}
|
||||
|
||||
/**
|
||||
* Log directly to file without going through console.
|
||||
* Useful for logging in hot paths where you don't want console output,
|
||||
* or when you want to log something that shouldn't appear in the browser console.
|
||||
*
|
||||
* Example:
|
||||
* logToFile('debug', 'Performance metric:', performanceData)
|
||||
*/
|
||||
export function logToFile(level: "log" | "warn" | "error" | "debug", ...args: unknown[]) {
|
||||
sendToExtension(level, args)
|
||||
}
|
||||
Reference in New Issue
Block a user