fix(widgets): expose per-edit application detail on patchWidget results#33
Open
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Open
fix(widgets): expose per-edit application detail on patchWidget results#33nsyring wants to merge 1 commit intoagent0ai:mainfrom
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Conversation
patchWidget previously confirmed source-level success without telling
the agent which kinds of edits were just applied or which entry caused
a multi-edit failure. With agent-generated multi-edit patches now
common (post the read-before-mutate skill rules), three concrete gaps
showed up in real traces:
- "patched, rendered ok" did not distinguish a single targeted change
from a twelve-edit refactor
- a thrown "find text was not found" did not name the failing edit
among 4 find/replace entries; the agent had to retry-and-narrow
- "find text is ambiguous" did not say how many duplicates existed
This change closes those gaps without altering the atomic semantics of
patchWidget (still all-or-nothing on the storage side):
- describeWidgetPatchEdit and buildWidgetPatchEditErrorPrefix turn an
edit's user-facing fields into a short label, used to prefix every
validation error with the edit index plus a description (e.g.
Edit 3 (find "const max = ..."): exact widget snippet edit `find`
text was not found in the readable renderer)
- the ambiguity error now names the actual occurrence count
- applyWidgetPatchEdits returns { rendererSource, appliedEdits }; the
appliedEdits array describes each applied edit with its kind and
position-relevant details (lineNumber for find/replace, from/to/
removedLines/insertedLines for line edits)
- patchWidget threads appliedEdits through buildWidgetWriteResult into
the result envelope so skill rules and eval harnesses can read
structured detail without parsing the status string
- formatWidgetOperationStatusText aggregates appliedEdits into a short
fragment between the verb and the render-status fragment:
"patched, 3 edits applied (2 find/replace, 1 line replace),
rendered ok, ..."
renderWidget, upsertWidget, and reloadWidget status text is
byte-identical to today (no appliedEdits on those paths).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(widgets): expose per-edit application detail on patchWidget results
Summary
patchWidgetpreviously returned a single status line that confirmed source-level success without telling the agent which kinds of edits were just applied or how many of them. With multi-edit patches now common in agent-generated calls (see widget-skillread before mutateflow), this PR adds a structuredappliedEdits: [...]field on the result envelope and a short aggregated fragment in the existing status string.After:
The structured
appliedEditsarray is also available on the tool result for skill rules and eval harnesses that want to read individual edit positions, replaced lengths, or removed/inserted line counts without parsing the status string.When patch validation fails, the error message now identifies which edit caused the failure by index plus a short edit description, so the agent does not have to guess which entry in a multi-edit array it has to fix.
Why
The widget-skill SKILL.md already requires that the agent build a fresh source view via
readWidgetbefore patching, and thepatch vs rewriterules pin the canonical edit shapes. But the validator and runtime were both opaque about which edits applied and which edit failed:Today: A
patchWidgetcall with five edits either succeeds or throws once with a generic "find text was not found" / "from line is outside range" message. Three problems show up in real agent traces:patched, rendered okstatus whether the call applied one targeted change or twelve. It re-derives a mental model from the chat history, which can drift.findtext. With fourfind/replaceentries in the call, the agent has to retry-and-narrow.findtext that matches twice produces "find text is ambiguous" without telling the agent how many duplicates exist or where they are. The agent often retries with a slightly longer snippet that also matches twice.This PR closes those three gaps without changing the atomic semantics of
patchWidget(the call is still all-or-nothing on the storage side; partial application is out of scope).What changed
app/L0/_all/mod/_core/spaces/storage.js(+91 / -16)describeWidgetPatchEdit(edit)andbuildWidgetPatchEditErrorPrefix(editIndex, edit)are new helpers that turn an edit's user-facing fields (find,from,to,line,range) into a short label likefind "snake_color = 'gr…"orline 264-265.normalizeWidgetPatchEdit(edit, lineCount, sourceText, editIndex = -1)accepts an edit index. All thrown errors from per-edit normalization now prependEdit ${editIndex+1} (${description}):so the agent readsEdit 3 (find "const max = ..."): exact widget snippet edit \find` text was not found in the readable renderer.` instead of the generic message. Cross-edit validation errors (overlap, mode-mixing, whole-renderer rewrite) reference two edits by their existing per-span labels and intentionally stay un-prefixed; their wording already names both spans.normalizeWidgetTextPatchEdit(...)carries the same prefix on its three failure paths and also includes the actual occurrence count in the ambiguity message:find text matches 4 renderer locations and is ambiguousinstead of justis ambiguous.applyWidgetPatchEdits(widgetRecord, edits)now returns{ rendererSource, appliedEdits }.appliedEditsis an array of{kind, ...details}entries describing each successfully applied edit:{kind: "find/replace", lineNumber, findLength, replacedLength, contentLength}for snippet edits, wherelineNumberis the renderer line the matched snippet started on (multi-line matches report their start line) andfindLengthis the length of the matchedfindtext — useful for skill rules that want to detect near-miss patterns{kind: "replace", from, to, removedLines, insertedLines}for ranged line edits{kind: "insert", from, insertedLines}for line insertsbuildWidgetWriteResult(spaceRecord, widgetId, extras = null)accepts a third argument and threadsappliedEditsfrom the caller into the result envelope. Existing two-arg callers (upsertWidget,upsertWidgets,removeWidgetpaths) are unchanged.patchWidget(...)captures theapplyWidgetPatchEditsoutcome and passesappliedEditstobuildWidgetWriteResult. The renderer source threading is otherwise identical.app/L0/_all/mod/_core/spaces/store.js(+47 / -7)formatWidgetAppliedEditsFragment(appliedEdits)aggregates the per-edit detail array into a short status fragment. It groups by edit kind, prints a count, and degrades to the empty string when noappliedEditsis present.formatWidgetOperationStatusText(...)accepts a newappliedEditsoption and inserts the fragment between the verb (patched) and the render-status fragment (rendered ok). The fragment is empty for non-patch operations (renderWidget, upsertWidget, reloadWidget) so their status text is byte-identical to today.buildWidgetToolResult(...)readsappliedEditsoff the result envelope and forwards it to the formatter.Behavior matrix
patchWidget1 find/replace editpatched, rendered ok, ...patched, 1 edit applied (1 find/replace), rendered ok, ...patchWidget3 mixed editspatched, rendered ok, ...patched, 3 edits applied (2 find/replace, 1 line replace), rendered ok, ...renderWidget/upsertWidgetreloadWidgetfindError: ... find text was not found ...Error: Edit 3 (find "const max = ..."): exact widget snippet edit \find` text was not found ...`Error: ... find text is ambiguous ...Error: Edit 1 (find "const x"): exact widget snippet edit \find` text matches 4 renderer locations and is ambiguous ...`No callers of
formatWidgetOperationStatusTextorbuildWidgetWriteResultneed to change. The structuralappliedEditsfield is purely additive.Test plan
node --checkon both modified files passesformatWidgetAppliedEditsFragmentsanity table verified for empty, undefined, single-edit, multi-edit, and mixed-kind casesnpm run desktop:packbuilds:findsnippet returns an error that names the edit index, helping the agent fix only that entry on retryRelationship to other PRs
Compatible with PR #6 ("prevent agent widget editing loops"). The widget-skill rule "If patchWidget() or renderWidget() says No files were written, the old widget file is still the source of truth. Fix and retry" pattern-matches on the keyword phrase
was not found. The newEdit N (description):prefix is prepended to the existing message, so every keyword phrase the skill rules match on (was not found,is ambiguous,outside the readable renderer range) is preserved verbatim. The agent additionally gets the index and short label of the failing edit so it can fix only that entry on retry instead of resubmitting the whole batch.Out of scope (possible follow-ups)
patchWidgetand is a deliberate separate discussion.find text not found. The current PR includes the location count for ambiguous matches but does not yet suggest near-misses; a Levenshtein or token-based suggestion would be useful diagnostic but adds nontrivial code.appliedEditsin theCurrent Widgettransient envelope. The structured field on the tool result is the canonical surface; transient header is decorative.🤖 Generated with Claude Code