Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ patch vs rewrite
- from and to are inclusive zero-based renderer line numbers
- Omit to to insert before from
- Omit content on a ranged line edit to delete
- Common line aliases like line, startLine/endLine, range, text, and replace are tolerated, but prefer the canonical shapes above
- Common coordinate aliases like line, startLine/endLine, and range are tolerated, but prefer the canonical shapes above
- Common content aliases like text, replace, value, replaceWith, replacement, and newText are tolerated, but prefer canonical content
- Other field names on an edit object are rejected fast with a list of accepted aliases. Do not invent fields like path, replaceText, or body — they are unrecognized and the edit will be refused before any line is touched
- Do not mix exact find edits and line edits in the same call
- Do not overlap edits
- The runtime applies edits from higher line numbers down to lower ones
Expand Down
89 changes: 64 additions & 25 deletions app/L0/_all/mod/_core/spaces/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -618,33 +618,60 @@ function isWidgetPatchTextLike(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}

function readWidgetPatchContentField(edit = {}) {
if (hasOwnWidgetPatchField(edit, "content")) {
return {
key: "content",
value: edit.content
};
}

if (hasOwnWidgetPatchField(edit, "text")) {
return {
key: "text",
value: edit.text
};
// Field names accepted as replacement-text aliases on a widget patch edit.
// `content` is canonical; `text`, `replace`, `value`, `replaceWith`,
// `replacement`, and `newText` are tolerated because LLM patch outputs
// frequently default to those names from VS Code, OpenAI tool-call,
// jsondiffpatch, and similar conventions. Listing them explicitly here
// also lets the validator surface a precise error message when none of the
// aliases is set on a `replace`-style line edit.
const WIDGET_PATCH_CONTENT_FIELD_ALIASES = Object.freeze([
"content",
"text",
"replace",
"value",
"replaceWith",
"replacement",
"newText"
]);

// All field names the widget patch validator recognizes on an edit object.
// Anything outside this set is unknown and almost certainly a typo'd content
// alias from an LLM (`with`, `replaceText`, `body`, `newContent`, ...). The
// validator surfaces unknown fields as an explicit error so the agent does
// not silently delete renderer lines when its replacement text is rejected
// by name mismatch.
const WIDGET_PATCH_KNOWN_EDIT_FIELDS = Object.freeze([
...WIDGET_PATCH_CONTENT_FIELD_ALIASES,
"find",
"search",
"from",
"to",
"line",
"startLine",
"endLine",
"range",
"mode",
"kind"
]);

function listUnknownWidgetPatchEditFields(edit = {}) {
if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
return [];
}

if (hasOwnWidgetPatchField(edit, "replace")) {
return {
key: "replace",
value: edit.replace
};
}
const knownFieldSet = new Set(WIDGET_PATCH_KNOWN_EDIT_FIELDS);
return Object.keys(edit).filter((key) => !knownFieldSet.has(key));
}

if (hasOwnWidgetPatchField(edit, "value")) {
return {
key: "value",
value: edit.value
};
function readWidgetPatchContentField(edit = {}) {
for (const aliasKey of WIDGET_PATCH_CONTENT_FIELD_ALIASES) {
if (hasOwnWidgetPatchField(edit, aliasKey)) {
return {
key: aliasKey,
value: edit[aliasKey]
};
}
}

return null;
Expand Down Expand Up @@ -732,6 +759,16 @@ function normalizeWidgetTextPatchEdit(edit, sourceText) {
function normalizeWidgetPatchEdit(edit, lineCount, sourceText) {
const normalizedEdit = edit && typeof edit === "object" ? edit : {};

const unknownFields = listUnknownWidgetPatchEditFields(normalizedEdit);
if (unknownFields.length > 0) {
throw new Error(
`Widget patch edit has unknown field${unknownFields.length === 1 ? "" : "s"} ${unknownFields.map((name) => `\`${name}\``).join(", ")}. ` +
`Valid replacement-text fields are ${WIDGET_PATCH_CONTENT_FIELD_ALIASES.map((name) => `\`${name}\``).join(", ")}; ` +
`valid coordinates are \`from\`/\`to\` (canonical) or \`line\`/\`startLine\`/\`endLine\`/\`range\` aliases; ` +
`exact-snippet edits use \`find\` (or \`search\`) plus a replacement field.`
);
}

if (hasOwnWidgetPatchField(normalizedEdit, "find") || hasOwnWidgetPatchField(normalizedEdit, "search")) {
return normalizeWidgetTextPatchEdit(normalizedEdit, sourceText);
}
Expand Down Expand Up @@ -765,7 +802,9 @@ function normalizeWidgetPatchEdit(edit, lineCount, sourceText) {
}

if (!hasTo && !hasContent) {
throw new Error("Insert edits must include replacement text in `content`.");
throw new Error(
`Insert edits must include replacement text. Use \`content\` (canonical) or one of: ${WIDGET_PATCH_CONTENT_FIELD_ALIASES.slice(1).map((name) => `\`${name}\``).join(", ")}.`
);
}

if (!hasTo) {
Expand Down