fix(home/pi): apply suggested edits atomically

Assisted-by: pi (openai-codex/gpt-5.6-sol)
This commit is contained in:
Gabriel Fontes
2026-07-17 17:21:19 -03:00
parent 0934fb9b0a
commit 51863d3a47
2 changed files with 76 additions and 30 deletions
@@ -18,6 +18,14 @@ const SUGGESTIONS_FILE =
const editDefinition = createEditToolDefinition(process.cwd());
type SuggestionEdit = {
replacement: string;
range: {
start: { line: number; character: number };
end: { line: number; character: number };
};
};
type Suggestion = {
id: string;
file: string;
@@ -25,10 +33,8 @@ type Suggestion = {
message: string;
replacement: string;
severity: number;
range: {
start: { line: number; character: number };
end: { line: number; character: number };
};
range: SuggestionEdit["range"];
edits?: SuggestionEdit[];
createdAt: string;
};
@@ -162,37 +168,53 @@ export default function suggestEdit(pi: ExtensionAPI) {
const content = normalizeToLf(
rawContent.startsWith("\uFEFF") ? rawContent.slice(1) : rawContent,
);
const matches = matchEdits(content, input.edits, input.path);
const createdAt = new Date().toISOString();
const callId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const suggestions = matches.map((match) => {
const ordinal =
matches.length > 1 ? ` ${match.editIndex + 1}/${matches.length}` : "";
return {
id: `${callId}-${match.editIndex}`,
file,
title: `Apply suggested edit${ordinal}`,
message: `LLM suggested edit${ordinal}`,
replacement: match.newText,
severity: 3,
range: {
start: lspPosition(content, match.start),
end: lspPosition(content, match.end),
},
createdAt,
} satisfies Suggestion;
});
const matches = matchEdits(content, input.edits, input.path)
.filter(
(match) => content.slice(match.start, match.end) !== match.newText,
)
.sort((a, b) => a.start - b.start);
if (matches.length === 0) {
throw new Error(
`No changes suggested for ${input.path}. The replacements produced identical content.`,
);
}
writeSuggestions([...readSuggestions(), ...suggestions]);
const edits = matches.map((match) => ({
replacement: match.newText,
range: {
start: lspPosition(content, match.start),
end: lspPosition(content, match.end),
},
}));
const firstEdit = edits[0];
const count = edits.length;
const suggestion = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
file,
title:
count === 1
? "Apply suggested edit"
: `Apply ${count} suggested edits`,
message:
count === 1 ? "LLM suggested edit" : `LLM suggested ${count} edits`,
// Keep the legacy fields so older readers can display the primary edit.
replacement: firstEdit.replacement,
severity: 3,
range: firstEdit.range,
edits,
createdAt: new Date().toISOString(),
} satisfies Suggestion;
writeSuggestions([...readSuggestions(), suggestion]);
return {
content: [
{
type: "text",
text: `Published ${suggestions.length} LLM suggested edit(s) for ${path.relative(ctx.cwd, file)}.`,
text: `Published 1 LLM suggestion containing ${count} edit(s) for ${path.relative(ctx.cwd, file)}.`,
},
],
details: { file, ids: suggestions.map((suggestion) => suggestion.id) },
details: { file, ids: [suggestion.id] },
};
},
});
+27 -3
View File
@@ -107,6 +107,30 @@ def diagnostic_for(item):
}
def text_edits_for(item):
edits = item.get("edits")
if isinstance(edits, list):
result = []
for edit in edits:
if not isinstance(edit, dict) or not isinstance(
edit.get("replacement"), str
):
continue
result.append(
{
"range": normalize_range(edit),
"newText": edit["replacement"],
}
)
if result:
return result
replacement = item.get("replacement")
if not isinstance(replacement, str):
return []
return [{"range": normalize_range(item), "newText": replacement}]
def publish(uri):
path = uri_to_path(uri)
if not path:
@@ -177,8 +201,8 @@ def code_actions(params):
request_range = params.get("range")
diagnostic_ids = context_diagnostic_ids(params)
for item in suggestions_for(path):
replacement = item.get("replacement")
if not isinstance(replacement, str):
text_edits = text_edits_for(item)
if not text_edits:
continue
if diagnostic_ids is not None and item.get("id") not in diagnostic_ids:
continue
@@ -191,7 +215,7 @@ def code_actions(params):
"kind": "quickfix",
"isPreferred": False,
"diagnostics": [diagnostic_for(item)],
"edit": {"changes": {uri: [{"range": item_range, "newText": replacement}]}},
"edit": {"changes": {uri: text_edits}},
}
if item.get("id") is not None:
action["command"] = {