mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c1f8bcb63 | |||
| ad86ab6740 | |||
| 97041c2df4 | |||
| 27a224c112 | |||
| bd5ba4e87b | |||
| 9b3736ae63 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat: background diff edit mode - allows cline to edit files in the background without stealing focus from your editor.
|
||||
+45
-45
@@ -1,47 +1,47 @@
|
||||
{
|
||||
"name": "cline-evals",
|
||||
"version": "0.1.0",
|
||||
"description": "Evaluation scripts and tools for Cline",
|
||||
"main": "cli/dist/index.js",
|
||||
"scripts": {
|
||||
"build:cli": "cd cli && tsc",
|
||||
"start:cli": "cd cli && node dist/index.js",
|
||||
"dev:cli": "cd cli && ts-node src/index.ts",
|
||||
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"evaluation",
|
||||
"benchmark",
|
||||
"diff-edits"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ora": "^5.4.1",
|
||||
"sqlite": "^4.1.2",
|
||||
"tiktoken": "^1.0.21",
|
||||
"uuid": "^9.0.0",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": "^3.1.1"
|
||||
}
|
||||
"name": "cline-evals",
|
||||
"version": "0.1.0",
|
||||
"description": "Evaluation scripts and tools for Cline",
|
||||
"main": "cli/dist/index.js",
|
||||
"scripts": {
|
||||
"build:cli": "cd cli && tsc",
|
||||
"start:cli": "cd cli && node dist/index.js",
|
||||
"dev:cli": "cd cli && ts-node src/index.ts",
|
||||
"diff-eval": "./diff-edits/run_and_open_dashboard.sh",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"evaluation",
|
||||
"benchmark",
|
||||
"diff-edits"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^16.5.0",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ora": "^5.4.1",
|
||||
"sqlite": "^4.1.2",
|
||||
"tiktoken": "^1.0.21",
|
||||
"uuid": "^9.0.0",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,7 @@ message UpdateSettingsRequest {
|
||||
optional int32 subagent_terminal_output_line_limit = 30;
|
||||
optional string cline_env = 31;
|
||||
optional bool native_tool_call_enabled = 32;
|
||||
optional bool background_edit_enabled = 33;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -65,6 +65,7 @@ enum ClineSay {
|
||||
INFO = 26;
|
||||
TASK_PROGRESS = 27;
|
||||
ERROR_RETRY = 28;
|
||||
DIFF_EDITING = 29;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -820,6 +820,7 @@ export class Controller {
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const backgroundEditEnabled = this.stateManager.getGlobalSettingsKey("backgroundEditEnabled")
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
@@ -915,6 +916,7 @@ export class Controller {
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
subagentsEnabled,
|
||||
backgroundEditEnabled,
|
||||
nativeToolCallSetting: {
|
||||
user: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
featureFlag: featureFlagsService.getNativeToolCallEnabled(),
|
||||
|
||||
@@ -349,6 +349,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
|
||||
}
|
||||
|
||||
if (request.backgroundEditEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("backgroundEditEnabled", !!request.backgroundEditEnabled)
|
||||
}
|
||||
|
||||
if (request.nativeToolCallEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
ClineDefaultTool.FILE_READ,
|
||||
ClineDefaultTool.FILE_NEW,
|
||||
ClineDefaultTool.FILE_EDIT,
|
||||
ClineDefaultTool.APPLY_PATCH,
|
||||
ClineDefaultTool.SEARCH,
|
||||
ClineDefaultTool.LIST_FILES,
|
||||
ClineDefaultTool.LIST_CODE_DEF,
|
||||
|
||||
@@ -15,10 +15,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.EDITING_FILES}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -296,6 +296,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
const openTelemetryLogMaxQueueSize =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogMaxQueueSize"]>("openTelemetryLogMaxQueueSize")
|
||||
const subagentsEnabled = context.globalState.get<GlobalStateAndSettings["subagentsEnabled"]>("subagentsEnabled")
|
||||
const backgroundEditEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["backgroundEditEnabled"]>("backgroundEditEnabled")
|
||||
|
||||
// Get mode-related configurations
|
||||
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
|
||||
@@ -622,6 +624,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
// Hooks require explicit user opt-in
|
||||
hooksEnabled: hooksEnabled ?? false,
|
||||
subagentsEnabled: subagentsEnabled ?? false,
|
||||
backgroundEditEnabled: backgroundEditEnabled ?? false,
|
||||
lastDismissedInfoBannerVersion: lastDismissedInfoBannerVersion ?? 0,
|
||||
lastDismissedModelBannerVersion: lastDismissedModelBannerVersion ?? 0,
|
||||
lastDismissedCliBannerVersion: lastDismissedCliBannerVersion ?? 0,
|
||||
|
||||
+21
-1
@@ -81,6 +81,7 @@ import type { SystemPromptContext } from "@/core/prompts/system-prompt"
|
||||
import { getSystemPrompt } from "@/core/prompts/system-prompt"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { isSubagentCommand, transformClineCommand } from "@/integrations/cli-subagents/subagent_command"
|
||||
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
|
||||
import { ClineError, ClineErrorType, ErrorService } from "@/services/error"
|
||||
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
@@ -273,12 +274,17 @@ export class Task {
|
||||
this.urlContentFetcher = new UrlContentFetcher(controller.context)
|
||||
this.browserSession = new BrowserSession(stateManager)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = HostProvider.get().createDiffViewProvider()
|
||||
this.toolUseHandler = new ToolUseHandler()
|
||||
|
||||
this.cwd = cwd
|
||||
this.stateManager = stateManager
|
||||
this.workspaceManager = workspaceManager
|
||||
|
||||
const backgroundEditSettingEnabled = this.stateManager.getGlobalSettingsKey("backgroundEditEnabled")
|
||||
this.diffViewProvider = backgroundEditSettingEnabled
|
||||
? new FileEditProvider()
|
||||
: HostProvider.get().createDiffViewProvider()
|
||||
|
||||
// Set up MCP notification callback for real-time notifications
|
||||
this.mcpHub.setNotificationCallback(async (serverName: string, _level: string, message: string) => {
|
||||
// Display notification in chat immediately
|
||||
@@ -650,6 +656,20 @@ export class Task {
|
||||
throw new Error("Cline instance aborted")
|
||||
}
|
||||
|
||||
if (type === "diff_editing") {
|
||||
this.messageStateHandler.setClineMessageInProgress({
|
||||
ts: 0,
|
||||
type: "say",
|
||||
say: type,
|
||||
text,
|
||||
images,
|
||||
files,
|
||||
partial,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (partial !== undefined) {
|
||||
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
|
||||
const isUpdatingPreviousPartial =
|
||||
|
||||
@@ -29,6 +29,12 @@ export class MessageStateHandler {
|
||||
private taskId: string
|
||||
private ulid: string
|
||||
private taskState: TaskState
|
||||
/**
|
||||
* The Cline message that is currently being streamed to the UI.
|
||||
* This message is not yet part of the clineMessages array until
|
||||
* it is marked as complete (partial === false).
|
||||
*/
|
||||
private clineMessageInProgress: ClineMessage | null = null
|
||||
|
||||
constructor(params: MessageStateHandlerParams) {
|
||||
this.taskId = params.taskId
|
||||
@@ -38,6 +44,22 @@ export class MessageStateHandler {
|
||||
this.updateTaskHistory = params.updateTaskHistory
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cline message that is currently being streamed.
|
||||
* If the message is marked as complete (partial === false),
|
||||
* it is added to the list of cline messages and the in-progress
|
||||
* message is cleared.
|
||||
* @param message The cline message to set as in-progress.
|
||||
*/
|
||||
setClineMessageInProgress(stream: ClineMessage) {
|
||||
const ts = this.clineMessageInProgress?.ts ?? Date.now()
|
||||
this.clineMessageInProgress = { ...stream, ts }
|
||||
if (stream.partial === false) {
|
||||
this.clineMessages.push(this.clineMessageInProgress)
|
||||
this.clineMessageInProgress = null
|
||||
}
|
||||
}
|
||||
|
||||
setCheckpointTracker(tracker: CheckpointTracker | undefined) {
|
||||
this.checkpointTracker = tracker
|
||||
}
|
||||
@@ -51,7 +73,12 @@ export class MessageStateHandler {
|
||||
}
|
||||
|
||||
getClineMessages(): ClineMessage[] {
|
||||
return this.clineMessages
|
||||
if (this.clineMessageInProgress === null) {
|
||||
return this.clineMessages
|
||||
}
|
||||
// Append the in-progress message to the list for display purposes.
|
||||
// We do not save this combined list until the in-progress message is complete.
|
||||
return [...this.clineMessages, this.clineMessageInProgress]
|
||||
}
|
||||
|
||||
setClineMessages(newMessages: ClineMessage[]) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,11 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
await uiHelpers.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
}
|
||||
|
||||
// const diffBlock = diff && generateDiff(diff)
|
||||
// if (diffBlock) {
|
||||
// await config.callbacks.say("diff_editing", diffBlock, undefined, undefined, true)
|
||||
// }
|
||||
|
||||
// CRITICAL: Open editor and stream content in real-time (from original code)
|
||||
if (!config.services.diffViewProvider.isEditing) {
|
||||
// Open the editor and prepare to stream content in
|
||||
@@ -130,6 +135,11 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
const { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext } = result
|
||||
|
||||
// const diffBlock = diff && generateDiff(diff)
|
||||
// if (diffBlock) {
|
||||
// await config.callbacks.say("diff_editing", diffBlock, undefined, undefined, true)
|
||||
// }
|
||||
|
||||
// Handle approval flow
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
@@ -450,3 +460,43 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
|
||||
}
|
||||
}
|
||||
|
||||
function generateDiff(block: string): string | undefined {
|
||||
// Regex to extract SEARCH and REPLACE blocks
|
||||
// Matches content between "------- SEARCH" and "=======" for search block
|
||||
// Matches content between "=======" and "+++++++ REPLACE" for replace block
|
||||
const searchRegex = /-------\s*SEARCH\s*\n([\s\S]*?)\n\s*=======/
|
||||
const replaceRegex = /=======\s*\n([\s\S]*?)\n\s*\+\+\+\+\+\+\+\s*REPLACE/
|
||||
|
||||
// Extract the blocks
|
||||
const searchMatch = block.match(searchRegex)
|
||||
const replaceMatch = block.match(replaceRegex)
|
||||
|
||||
if (!searchMatch || !replaceMatch) {
|
||||
// throw new Error('Invalid diff block format. Expected "------- SEARCH", "=======", and "+++++++ REPLACE" markers.')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const searchBlock = searchMatch[1]
|
||||
const replaceBlock = replaceMatch[1]
|
||||
|
||||
// Split blocks into lines
|
||||
const searchLines = searchBlock.split("\n")
|
||||
const replaceLines = replaceBlock.split("\n")
|
||||
|
||||
// Build the diff output
|
||||
const diffLines: string[] = []
|
||||
|
||||
// Add all search lines with "- " prefix
|
||||
for (const line of searchLines) {
|
||||
diffLines.push(`- ${line}`)
|
||||
}
|
||||
|
||||
// Add all replace lines with "+ " prefix
|
||||
for (const line of replaceLines) {
|
||||
diffLines.push(`+ ${line}`)
|
||||
}
|
||||
|
||||
// Join into final diff string
|
||||
return diffLines.join("\n")
|
||||
}
|
||||
|
||||
@@ -150,6 +150,13 @@ export abstract class DiffViewProvider {
|
||||
*/
|
||||
protected abstract resetDiffView(): Promise<void>
|
||||
|
||||
/**
|
||||
* Shows the document in the editor.
|
||||
*/
|
||||
async showDocument(absolutePath: string): Promise<void> {
|
||||
await openFile(absolutePath, true)
|
||||
}
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
@@ -268,7 +275,7 @@ export abstract class DiffViewProvider {
|
||||
// get text after save in case there is any auto-formatting done by the editor
|
||||
const postSaveContent = (await this.getDocumentText()) || ""
|
||||
|
||||
await this.showFile(this.absolutePath)
|
||||
await this.showDocument(this.absolutePath)
|
||||
await this.closeAllDiffViews()
|
||||
|
||||
const newProblems = await this.getNewDiagnosticProblems()
|
||||
@@ -372,6 +379,26 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile() {
|
||||
if (!this.absolutePath || !this.isEditing) {
|
||||
return
|
||||
}
|
||||
|
||||
// Close diff views before deleting the file
|
||||
await this.closeAllDiffViews()
|
||||
|
||||
// Delete the file
|
||||
try {
|
||||
await fs.rm(this.absolutePath, { force: true })
|
||||
console.log(`File ${this.absolutePath} has been deleted.`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete file ${this.absolutePath}:`, error)
|
||||
}
|
||||
|
||||
// edit is done
|
||||
await this.reset()
|
||||
}
|
||||
|
||||
// close editor if open?
|
||||
async reset() {
|
||||
this.isEditing = false
|
||||
|
||||
@@ -16,11 +16,6 @@ export class FileEditProvider extends DiffViewProvider {
|
||||
super()
|
||||
}
|
||||
|
||||
override showFile(_absolutePath: string): Promise<void> {
|
||||
// No-op: No visual editor to show the file
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
protected async openDiffEditor(): Promise<void> {
|
||||
// No-op: No visual editor to open in a file-system-only provider
|
||||
// The file content is already loaded in the base class's open() method
|
||||
@@ -58,6 +53,10 @@ export class FileEditProvider extends DiffViewProvider {
|
||||
this.documentContent = lines.join("\n")
|
||||
}
|
||||
|
||||
override async showDocument(_absolutePath: string): Promise<void> {
|
||||
// No-op: No visual editor to show the file
|
||||
}
|
||||
|
||||
protected async scrollEditorToLine(_line: number): Promise<void> {
|
||||
// No-op: No visual editor to scroll
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface ExtensionState {
|
||||
remoteConfigSettings?: Partial<GlobalStateAndSettings>
|
||||
subagentsEnabled?: boolean
|
||||
nativeToolCallSetting?: ClineFeatureSetting
|
||||
backgroundEditEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
@@ -167,6 +168,7 @@ export type ClineSay =
|
||||
| "load_mcp_documentation"
|
||||
| "info" // Added for general informational messages like retry status
|
||||
| "task_progress"
|
||||
| "diff_editing"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
|
||||
@@ -100,6 +100,7 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
|
||||
info: ClineSay.INFO,
|
||||
task_progress: ClineSay.TASK_PROGRESS,
|
||||
error_retry: ClineSay.ERROR_RETRY,
|
||||
diff_editing: ClineSay.DIFF_EDITING,
|
||||
}
|
||||
|
||||
const result = mapping[say]
|
||||
@@ -146,6 +147,7 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
|
||||
[ClineSay.INFO]: "info",
|
||||
[ClineSay.TASK_PROGRESS]: "task_progress",
|
||||
[ClineSay.ERROR_RETRY]: "error_retry",
|
||||
[ClineSay.DIFF_EDITING]: "diff_editing",
|
||||
}
|
||||
|
||||
return mapping[say]
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface Settings {
|
||||
ocaMode: string | undefined
|
||||
hooksEnabled: boolean
|
||||
subagentsEnabled: boolean
|
||||
backgroundEditEnabled: boolean
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: ApiProvider
|
||||
|
||||
@@ -34,6 +34,7 @@ import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import { DiffEditRow } from "./DiffEditRow"
|
||||
import { ErrorBlockTitle } from "./ErrorBlockTitle"
|
||||
import ErrorRow from "./ErrorRow"
|
||||
import NewTaskPreview from "./NewTaskPreview"
|
||||
@@ -865,6 +866,17 @@ export const ChatRowContent = memo(
|
||||
}
|
||||
}, [isCommandMessage, isCommandExecuting, isExpanded, onToggleExpand, message.ts])
|
||||
|
||||
if (message.say === "diff_editing" && message.text) {
|
||||
return (
|
||||
<div className="flex flex-col w-full">
|
||||
{/* <div className="flex items-center">
|
||||
<PenIcon className="size-2 stroke-1 mr-2 font-bold" /> Applying Patch
|
||||
</div> */}
|
||||
<DiffEditRow patch={message.text} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (message.ask === "command" || message.say === "command") {
|
||||
const splitMessage = (text: string) => {
|
||||
const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { ChevronDown, ChevronRight, ChevronsDownUpIcon, FilePlus, FileText, FileX } from "lucide-react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface Patch {
|
||||
action: string
|
||||
path: string
|
||||
lines: string[]
|
||||
additions: number
|
||||
deletions: number
|
||||
}
|
||||
|
||||
export const DiffEditRow: React.FC<{ patch: string }> = ({ patch }) => {
|
||||
const [isStreaming, setIsStreaming] = useState<boolean>(true)
|
||||
const [parsedFiles, setParsedFiles] = useState<Patch[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const started = patch.includes("*** Begin Patch")
|
||||
const ended = patch.includes("*** End Patch")
|
||||
if (started && ended) {
|
||||
setIsStreaming(false)
|
||||
}
|
||||
if (!started) {
|
||||
return
|
||||
}
|
||||
// Extract patches between the begin and end markers
|
||||
const parts = patch.split("*** Begin Patch")[1].split("*** End Patch")
|
||||
const patchContent = parts[0].trim()
|
||||
const patches = patchContent.split("\n*** ").map((p) => p.trim())
|
||||
|
||||
const parsed = parsePatchContent(patches.join("\n"))
|
||||
if (patches.length > 0 && parsed.length > 0) {
|
||||
setParsedFiles(parsed)
|
||||
}
|
||||
}, [patch])
|
||||
|
||||
if (parsedFiles.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 border border-code-block-background/70 rounded-sm">
|
||||
{parsedFiles.map((file, _idx) => (
|
||||
<FileBlock file={file} key={file.path} />
|
||||
))}
|
||||
{isStreaming && (
|
||||
<div className="bg-gray-800 rounded-lg flex items-center text-gray-400">
|
||||
<div className="animate-pulse mr-3">●</div>
|
||||
Streaming content...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FileBlock: React.FC<{ file: Patch }> = ({ file }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
|
||||
const getActionIcon = (action: string) => {
|
||||
switch (action) {
|
||||
case "Add":
|
||||
return <FilePlus className="w-5 h-5 text-success" />
|
||||
case "Delete":
|
||||
return <FileX className="w-5 h-5 text-error" />
|
||||
default:
|
||||
return <FileText className="w-5 h-5 text-info" />
|
||||
}
|
||||
}
|
||||
|
||||
const getActionColor = (action: string) => {
|
||||
switch (action) {
|
||||
case "Add":
|
||||
return "border-l-success"
|
||||
case "Delete":
|
||||
return "border-l-error"
|
||||
default:
|
||||
return "border-l-background"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-1 bg-editor-background rounded-lg border border-gray-800">
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-4 py-3 bg-editor-background hover:bg-gray-850 transition-colors rounded-t-lg border-b border-gray-800"
|
||||
onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="flex items-center gap-3">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
)}
|
||||
<span className={cn("flex items-center gap-2", getActionColor(file.action))}>
|
||||
{getActionIcon(file.action)}
|
||||
<span className="font-medium">{file.action}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{file.additions > 0 && <span className="text-success">+{file.additions}</span>}
|
||||
{file.additions > 0 && file.deletions > 0 && <span className="mx-1">·</span>}
|
||||
{file.deletions > 0 && <span className="text-error">-{file.deletions}</span>}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t border-code-block-background">
|
||||
<div className="font-mono text-xs">
|
||||
{file.lines.map((line, _idx) => (
|
||||
<DiffLine key={line} line={line} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DiffLine: React.FC<{ line: string }> = ({ line }) => {
|
||||
const getLineStyle = () => {
|
||||
if (line.startsWith("+")) {
|
||||
return "bg-green-900/30 text-success border-l-1 border-green-500"
|
||||
} else if (line.startsWith("-")) {
|
||||
return "bg-red-900/30 text-error border-l-1 border-red-500"
|
||||
} else {
|
||||
return "bg-editor-background text-editor-foreground"
|
||||
}
|
||||
}
|
||||
|
||||
if (line.trim() === "@@") {
|
||||
return (
|
||||
<div className="inline-flex items-center px-3 py-1 text-xs font-mono bg-description/10 w-full text-description">
|
||||
<ChevronsDownUpIcon className="size-2 mr-2" />
|
||||
@@
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("px-4 py-1 text-xs font-mono w-full", getLineStyle())}>
|
||||
<span>{line}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const parsePatchContent = (content: string) => {
|
||||
const files: Patch[] = []
|
||||
const lines = content.split("\n")
|
||||
|
||||
let currentFile: Patch | null = null
|
||||
|
||||
for (const line of lines) {
|
||||
const fileMatch = line.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/)
|
||||
|
||||
if (fileMatch) {
|
||||
if (currentFile) {
|
||||
files.push(currentFile)
|
||||
}
|
||||
|
||||
currentFile = {
|
||||
action: fileMatch[1],
|
||||
path: fileMatch[2].trim(),
|
||||
lines: [],
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
}
|
||||
} else if (currentFile && line.trim()) {
|
||||
currentFile.lines.push(line)
|
||||
|
||||
if (line.startsWith("+")) {
|
||||
currentFile.additions++
|
||||
} else if (line.startsWith("-")) {
|
||||
currentFile.deletions++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFile) {
|
||||
files.push(currentFile)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
@@ -56,6 +56,8 @@ const TaskTimelineTooltip = ({ message, children }: TaskTimelineTooltipProps) =>
|
||||
return "Task Completed"
|
||||
case "checkpoint_created":
|
||||
return "Checkpoint Created"
|
||||
case "diff_editing":
|
||||
return "Diff Editing"
|
||||
default:
|
||||
return message.say || "Unknown"
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export const getColor = (message: ClineMessage): string => {
|
||||
return COLOR_PURPLE // Purple for browser actions
|
||||
case "completion_result":
|
||||
return COLOR_GREEN // Green for task success
|
||||
case "diff_editing":
|
||||
return COLOR_BLUE // Green for task success
|
||||
default:
|
||||
return COLOR_DARK_GRAY // Dark gray for unknown
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
remoteConfigSettings,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting,
|
||||
backgroundEditEnabled,
|
||||
} = useExtensionState()
|
||||
|
||||
const [isClineCliInstalled, setIsClineCliInstalled] = useState(false)
|
||||
@@ -152,7 +153,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs mt-1 mb-0">
|
||||
<span className="text-[var(--vscode-errorForeground)]">Experimental: </span>{" "}
|
||||
<span className="text-input-error-foreground">Experimental: </span>{" "}
|
||||
<span className="text-description">
|
||||
Allows Cline to spawn subprocesses to handle focused tasks like exploring large codebases,
|
||||
keeping your main context clean.
|
||||
@@ -356,6 +357,24 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={backgroundEditEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("backgroundEditEnabled", checked)
|
||||
}}>
|
||||
Enable Background Edit
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs">
|
||||
<span className="text-xs bg-button-background/80 text-button-foreground px-2 py-1 rounded-lg mr-1">
|
||||
Experimental
|
||||
</span>
|
||||
<span className="text-description">
|
||||
Allows Cline to edit documents in the background without stealing focus from your editor.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{multiRootSetting.featureFlag && (
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
|
||||
@@ -225,6 +225,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
backgroundCommandTaskId: undefined,
|
||||
lastDismissedCliBannerVersion: 0,
|
||||
subagentsEnabled: false,
|
||||
backgroundEditEnabled: false,
|
||||
|
||||
// NEW: Add workspace information with defaults
|
||||
workspaceRoots: [],
|
||||
|
||||
Reference in New Issue
Block a user