Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a37dd24
feat(file-safety): atomic text publish primitive + safeWriteJson refa…
easonliang28 Aug 27, 2026
991ab69
feat(editor): async post-save diagnostics on chat-diff save path (L1,…
easonliang28 Aug 27, 2026
cb13606
chore(ci): empty commit — re-trigger CI and the CodeRabbit current-he…
easonliang28 Aug 30, 2026
082425a
merge(upstream): take main into feat/async-save-diagnostics-l1
Oct 6, 2026
16caa6f
fix(file-safety): publish through a dangling symlink instead of repla…
Oct 6, 2026
83186ab
fix(file-safety): distinguish readlink failures from an absent path, …
Oct 6, 2026
6e0ff88
fix(file-safety): remove the staging directory after the write, and g…
Oct 6, 2026
a606b41
fix(utils): stage the JSON temp with the target's mode instead of the…
Oct 6, 2026
0278f02
fix(utils): keep the staged JSON file owner-writable and surface non-…
Oct 6, 2026
ebc0aaf
fix(utils): key the safeWriteJson lock on the canonical publish target
Oct 6, 2026
55db013
fix(file-safety): default the staging mode only when the target is ge…
Oct 7, 2026
a953819
fix(mcp): confine a project-scoped settings write to the workspace
Oct 7, 2026
0a3b945
fix(mcp): canonicalize both sides of the confinement check
Oct 7, 2026
29819db
fix(file-safety): stop guessing when a scope or a mode cannot be read
Oct 7, 2026
3fcb137
chore: trigger a fresh review pass at this head
Oct 7, 2026
ad9764d
fix(file-safety): follow whole symlink chains and lock canonical paths
Oct 7, 2026
d88a438
fix(utils): keep the caller's path for the locked operation
Oct 7, 2026
0c88df6
fix(file-safety): retry transient Windows commit-rename failures
Oct 7, 2026
59bb20a
fix(mcp,editor): confine by source, retry the backup rename, restore …
Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@
},
"integrations/editor/__tests__/DiffViewProvider.spec.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 310
"count": 306
}
},
"integrations/editor/__tests__/EditorUtils.spec.ts": {
Expand Down Expand Up @@ -1716,7 +1716,7 @@
},
"utils/safeWriteJson.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 4
"count": 3
}
},
"utils/tts.ts": {
Expand Down
98 changes: 68 additions & 30 deletions src/integrations/editor/DiffViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { arePathsEqual, getReadablePath } from "../../utils/path"
import { formatResponse } from "../../core/prompts/responses"
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
import { Task } from "../../core/task/Task"
import { safeWriteText } from "../../services/file-safety/safeWriteText"

import { DecorationController } from "./DecorationController"

Expand Down Expand Up @@ -1151,12 +1152,15 @@ export class DiffViewProvider {
}> {
const absolutePath = path.resolve(this.cwd, relPath)

// Get diagnostics before editing the file
this.preDiagnostics = vscode.languages.getDiagnostics()
// Get diagnostics before editing the file. Capture the snapshot locally:
// overlapping saveDirectly calls (multi-file edits) must not let a later
// call overwrite this one's baseline before its diagnostics tail runs.
const preDiagnostics = vscode.languages.getDiagnostics()
this.preDiagnostics = preDiagnostics

// Write the content directly to the file
await createDirectoriesForFile(absolutePath)
await fs.writeFile(absolutePath, content, "utf-8")
await safeWriteText(absolutePath, content)

// Open the document to ensure diagnostics are loaded
// When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open in memory
Expand All @@ -1175,23 +1179,64 @@ export class DiffViewProvider {
await doc.save()
}

// Force a small delay to ensure diagnostics are triggered
await new Promise((resolve) => setTimeout(resolve, 100))
// The 100 ms diagnostics-settle wait is carried by the
// emitPostSaveDiagnostics tail (inMemoryDocument) instead of here:
// blocking the save path delayed every openFile=false save even when
// diagnostics were disabled or the write delay was 0.
}

let newProblemsMessage = ""

// L1 (A2): resolve without awaiting the LSP diagnostics settle. The
// diagnostics check becomes a fire-and-forget tail that emits any new
// problems via the existing "error" ClineSay type; the returned
// newProblemsMessage is therefore always undefined.
if (diagnosticsEnabled) {
// Add configurable delay to allow linters time to process
const safeDelayMs = Math.max(0, writeDelayMs)
// The method's outer try/catch guarantees it never rejects, so the
// fire-and-forget call needs no .catch wrapper.
void this.emitPostSaveDiagnostics(relPath, writeDelayMs, preDiagnostics, !openFile)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try {
await delay(safeDelayMs)
} catch (error) {
console.warn(`Failed to apply write delay: ${error}`)
}
// Store the results for formatFileWriteResponse
this.newProblemsMessage = undefined
this.userEdits = undefined
this.relPath = relPath
this.newContent = content

const postDiagnostics = vscode.languages.getDiagnostics()
return {
newProblemsMessage: undefined,
userEdits: undefined,
finalContent: content,
}
}

// L1 (A2): fire-and-forget post-save diagnostics. After the write delay,
// collects new Error-severity problems and emits them via the existing
// "error" ClineSay type (only Error-severity diagnostics reach this branch;
// "error" carries no task-failure semantics in core). Abort-safe: say()
// rejects when the task is aborted, so the whole body sits inside a
// try/catch that degrades to a console.warn — the tail can never reject.
private async emitPostSaveDiagnostics(
relPath: string,
writeDelayMs: number,
preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][],
inMemoryDocument = false,
): Promise<void> {
try {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Add configurable delay to allow linters time to process. When the
// document was opened in memory (openFile=false), the tail also
// carries the 100 ms diagnostics-settle wait that used to block
// saveDirectly. delay() never rejects, so no catch is required here.
const safeDelayMs = Math.max(0, writeDelayMs) + (inMemoryDocument ? 100 : 0)
await delay(safeDelayMs)

// Filter to the saved file: saveDirectly resolves before this tail
// completes, so in a multi-file write sequence (e.g. apply_patch)
// a later file's problems must not be attributed to this relPath.
const savedFilePath = path.resolve(this.cwd, relPath)
// arePathsEqual: case-insensitive on Windows, where a relPath whose
// casing differs from the diagnostic URI is still the same file.
const postDiagnostics = vscode.languages
.getDiagnostics()
.filter(([uri]) => arePathsEqual(uri.fsPath, savedFilePath))

// Get diagnostic settings from state
const task = this.taskRef.deref()
Expand All @@ -1200,27 +1245,20 @@ export class DiffViewProvider {
const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50

const newProblems = await diagnosticsToProblemsString(
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
getNewDiagnostics(preDiagnostics, postDiagnostics),
[vscode.DiagnosticSeverity.Error],
this.cwd,
includeDiagnosticMessages,
maxDiagnosticMessages,
)

newProblemsMessage =
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
}

// Store the results for formatFileWriteResponse
this.newProblemsMessage = newProblemsMessage
this.userEdits = undefined
this.relPath = relPath
this.newContent = content

return {
newProblemsMessage,
userEdits: undefined,
finalContent: content,
if (newProblems.length > 0) {
await task?.say("error", `New problems detected after saving file: ${relPath}\n\n${newProblems}`)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (error) {
// Abort-safe: never let a post-save diagnostic emit become an
// unhandled rejection (say() rejects when the task is aborted).
console.warn(`Post-save diagnostics emit failed: ${error}`)
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
}
}
}
Loading
Loading