Skip to content

feat(frontend): show the frozen content and diff on commit approvals - #5762

Merged
mmabrouk merged 6 commits into
release/v0.110.0from
agent-config-editing-s3b-wire-web
Aug 7, 2026
Merged

feat(frontend): show the frozen content and diff on commit approvals#5762
mmabrouk merged 6 commits into
release/v0.110.0from
agent-config-editing-s3b-wire-web

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 5, 2026

Copy link
Copy Markdown
Member

Context

When an agent decides to save a new version of itself, the run pauses and the approval dock asks you to approve the commit. Until now that card could not show you what you were approving.

The reason is the shape of the tool call. When the agent commits content from a file in its workspace, its arguments carry a marker, not the bytes:

{"operation": "set", "target": ["instructions"], "value": {"@ag.file": "instructions.md"}}

That is a path and nothing else. The bytes live in the sandbox. The runner resolves them at the gate, freezes them, and builds an approval manifest with the file sizes, digests, executable bits, and a unified diff against the text being replaced. The manifest cannot ride inside the tool input, because the input is the model's own arguments and must stay that way, and it cannot ride on the approval request either, because that object has a strict key set. So it arrives as a sibling data-approval-manifest part, and the frontend had no code reading it. You saw a path, and Approve was one click away.

This lane wires that manifest into the card. It targets its stack parent agent-config-editing-s3b-wire-py and is part of the agent-config-editing stack.

Changes

ApprovedContentManifest is the new component that renders what a commit imports: a unified diff per replaced field with added and removed line counts, then each imported file with its path, size, executable bit, and digest, then a sentence stating that the digest covers the full content and not just the part shown. It renders the runner's diff text as-is. DiffView from @agenta/ui is deliberately not used, because it normalizes content as JSON or YAML and would mangle a Markdown instructions document.

ApprovalDock now collects manifests from the turn's sibling parts, keys them by tool call id, and hands the right one to the card. transcriptToMessages emits the same data part when a stored transcript replays, so a page reload renders the same card instead of a poorer one.

The card shows this in both UI modes. Chat mode gets it inside the two-pane commit body, beside the "What's changing" summary. Build mode and any host without a selected revision get it under the raw payload block. This matters because Build mode is the default: the config panel is open unless you maximize the chat, so it is the card most approvals actually get.

Before, in Build mode, the whole card body was the payload block:

Payload  {"workflow_revision":{"delta":{"operations":[{"operation":"set","target":["instructions"],"value":{"@ag.file":"instructions.md"}}]}}}

After, the same gate adds the substance under it:

Replace instructions   +12  -3   1.2 KB -> 2.4 KB
@@ -1,4 +1,6 @@
-Be terse.
+Be brief and cite your sources.

From your workspace (2.4 KB)
instructions.md   2.4 KB   3f9a1c2b8e04

Approving commits exactly this content (digest 0123456789ab, covering the full text, not only what is shown).

The commit card also stops presenting the persisted commit message as the agent's intent. Those are two different things. The per-call description describes the call to the human watching and is never saved. The message inside workflow_revision is the commit message and is saved on the revision. The card used to render only the message, unlabelled, in the position where a reader takes it as the agent explaining itself. It now reads description and shows it under "What the agent says it is doing", with the message below it under "Commit message". A description longer than the catalog's 500-character cap is cut and marked as shortened.

Tests / notes

The AgentChatSlice suite runs 95 tests across 15 files, all passing, plus a clean tsc --noEmit over web/oss. Most of that is new: ApprovedContentManifest.test.tsx covers the diff, file, and digest rendering including truncation; ApprovalDock.test.tsx covers which branch renders the manifest; CommitRevisionApproval.test.tsx covers both card shapes with and without imported content and with a distinct description and message; transcriptToMessages.test.ts covers the replay mirror.

Two notes for a reviewer. The dock tests use the real registry, card, and manifest components and mock only the presentational shells, so the "renders exactly once in Chat mode" assertion is real and not an artifact of a stub. And the manifest tests were checked for vacuity: with the manifest render disabled in place, exactly the three Build-mode assertions failed and the rest passed.

One deliberate limit. Build mode still shows the raw payload plus the manifest rather than the full two-pane commit body. Enabling the specialized body in Build mode is a separate UX decision, tracked in docs/design/agent-config-editing/open-issues.md. This lane makes the content visible everywhere first, because that is the safety property.

What to QA

  • In the playground, open an agent that can edit itself and ask it to rewrite its instructions from a file in its workspace, then commit. Leave the layout alone so you stay in Build mode. When the approval dock appears, you see a unified diff, the file with its size and digest, and the sentence about the digest covering the full text. Approve sits right below it.
  • Maximize the chat panel and trigger the same kind of commit. The card becomes two panes. The left pane shows "What the agent says it is doing" and "Commit message" as separate labelled blocks. The right pane shows "What's changing" and the same diff. Check the imported content appears once, not twice.
  • Reload the page while the run is parked on that approval. After the transcript replays, the card shows the same diff, files, and digest. A reload must not downgrade the card to bare JSON.
  • Ask for a commit that changes a field directly, with no file import. The card shows the changes summary and no manifest block. An empty "From your workspace" heading is a bug.
  • Ask the agent to write a long explanation for the call. The intent text is cut at 500 characters and ends with "(shortened)".
  • Regression: trigger a gate for a different tool, such as a shell command or an MCP tool. That card is unchanged, shows the plain payload, and still approves and denies normally.
  • Regression: approve a commit and open the new revision. The saved content matches the diff you were shown, and the version bumps. The approval binds the exact bytes in the manifest, so a mismatch here is the serious failure.

Added by the E2E campaign and review fix rounds (5 Aug, evening)

  • The card now describes ordered-operations commits in BOTH modes: the root cause of raw-JSON cards was the preview classifier reading only the legacy delta form. A new reader describes the requested operations (verb, readable target, Now/After blocks with the real stored value) without porting the engine; imports keep the runner's authoritative manifest and lead the card; diff sections open by default on the card only.
  • New test classes: a host-level mount with real atoms and registry, and a production-shaped operations payload through the REAL classifier (the one that fails without the fix).

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 7, 2026 10:18am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 211d811e-ae4f-459d-8e17-6f495e87c13e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

// The manifest is a SIBLING of the payload, never inside it, so the generic card has to render
// it itself: the frozen content is what the approval binds, in every mode. Skipped when a
// specialized body is active, because that body renders the manifest already.
const fallbackManifest = useMemo(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this memo has a gate

Two components can render the same manifest. The specialized commit body renders it in Chat mode. This generic card renders it in every other mode. Both read the same current.manifest value.

The renderer ? null gate keeps only one of them active. When a specialized body is present, this memo returns null. The specialized body then owns the render.

If you remove the gate, Chat mode shows the diff, the file list, and the digest two times on one card. A reader can understand that as two separate changes to approve.

If you move the render out of the else branch, you get the same double render. The specialized body still draws its own copy.

The current?.manifest dependency matters as well. A turn can hold several gates. The dock steps through them one at a time. The memo must recompute when the shown gate changes, or the card keeps the content of the previous gate.

}

/** Manifests keyed by toolCallId, from the egress's `data-approval-manifest` sibling parts. */
const manifestsByToolCallId = (parts: UIMessage["parts"] = []): Map<string, unknown> => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the manifest arrives beside the tool call and not inside it

This function reads the data-approval-manifest parts of the turn. It keys each manifest by its tool call id. The dock then joins each pending gate to its own manifest.

The manifest cannot travel inside the tool input. There are two reasons.

The first reason is ownership. The tool input holds the arguments that the model wrote. The manifest holds bytes that the runner read from the sandbox. If we merge them, the card shows the user a payload that the model never sent. The user must be able to trust that the payload is the model's own text.

The second reason is the frame format. The tool-approval-request frame is a strict object with a fixed key set. It cannot carry an extra field.

The join is therefore load bearing. If the tool call id does not match, the card falls back to the raw arguments. Those arguments hold a file marker such as {"@ag.file": "instructions.md"}. That is a path and nothing more. The user then approves bytes that the user never saw.

}
// Mirrors the live egress's sibling data part, so a reloaded transcript renders the
// same card. `tool-approval-request` is a strict object and cannot carry this.
if (reqPayload.manifest !== undefined) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why replay rebuilds the part instead of reading the manifest directly

The live stream sends the manifest as a separate data part. A page reload does not replay the live stream. A reload reads the stored session records. This block turns the stored gate record back into the same part shape that the live stream produces.

The card therefore reads one shape only. It does not test whether the run is live or reloaded. That keeps the live path and the replay path from drifting apart over time.

If you delete this block, a reload degrades the card. The gate then shows the raw arguments, and those arguments hold a file path only. Approve stays active in that state. A user can approve content that the card never displayed. This is the exact state the card must never reach.

One related fact for a cold reader. The runner attaches the manifest to the emitted event, but it does not write the resolved content into the durable interaction row. The transcript record is the only replay source for this data.

* The agent's own note about this call (R12), labelled so it reads as a claim and not as a fact
* about what the commit does. The real diff sits beside it (read-config.md section 12.2, rule 5).
*/
const StatedIntent = ({intent}: {intent: CallDescription}) => (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the card labels two texts that look similar

This block shows the per-call description. The commit message gets a separate label below it.

The two texts are different things.

The description sits at the top level of the tool input, beside workflow_revision. It explains this call to the person who is watching. The runner deletes it before it builds the HTTP request, so the platform never stores it. The card keeps it because the runner strips it at dispatch time, after the gate.

The message sits inside workflow_revision. It is the commit message. The platform saves it on the new revision and shows it in the history.

The card previously showed the message alone, and it showed it with no label. A reader takes an unlabelled quote in that position as the agent explaining the call. That reading is wrong, because the message describes the change in the history.

Both texts come from the model. Neither text is a fact about what the call does. The labels separate them, and the real diff stays beside them. If you remove the labels, the card again presents stored history as live intent.

The contract for this rule is docs/design/agent-config-editing/contracts/read-config.md, section 12.2.

@mmabrouk

mmabrouk commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 4cc3fb3 to 7f77f92 Compare August 5, 2026 20:31
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 2e6b425 to 8a32996 Compare August 5, 2026 20:32
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 7f77f92 to e80845e Compare August 5, 2026 21:25
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 8a32996 to d75f18b Compare August 5, 2026 21:26
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from e80845e to 8d275fd Compare August 5, 2026 21:54
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from d75f18b to 00e5676 Compare August 5, 2026 21:55
@mmabrouk

mmabrouk commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 0837141 to 67d75b2 Compare August 6, 2026 14:28
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 0b3b5ad to 92e7619 Compare August 6, 2026 14:28
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 67d75b2 to 10e7d71 Compare August 6, 2026 14:49
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 92e7619 to 803ae6d Compare August 6, 2026 14:49
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 10e7d71 to 28316b4 Compare August 6, 2026 15:03
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 803ae6d to e44019a Compare August 6, 2026 15:04
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 28316b4 to 395f4ea Compare August 6, 2026 15:09
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from e44019a to c19ceaf Compare August 6, 2026 15:10
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 395f4ea to 2be2849 Compare August 6, 2026 15:19
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from c19ceaf to d7dac02 Compare August 6, 2026 15:19
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 2be2849 to 6a9d9d7 Compare August 6, 2026 15:26
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from d7dac02 to 1d88425 Compare August 6, 2026 15:27
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-py branch from 6a9d9d7 to aa02582 Compare August 6, 2026 15:49
@mmabrouk
mmabrouk force-pushed the agent-config-editing-s3b-wire-web branch from 1d88425 to ae7c8c3 Compare August 6, 2026 15:49

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

The approval dock renders the frozen manifest and the base-bound diff
through the existing commit_revision registry body: file list with
sizes, digests, and executable flags, single-file mode as a unified
diff. One consumer serves the live data part and the replayed
transcript; the manifest resolves by toolCallId on both paths.
…ecked)

renderToStaticMarkup over a new test-library dependency: the tree
executes, props flow, every branch runs; interaction stays untested and
says so. Brief corrections recorded: the 50-file cap is unreachable
(8-marker commit cap) and deliberately untested; the 400-line cap is
runner-side and the card asserts its diffTruncated sentence.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5762.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5762-0d2d23e
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-07T10:27:53.038Z

…-removal refactor (keep the Badge idiom from release, keep the defaultOpen/sectionOpenState feature from the lane, drop the stale antd imports)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Request New feature or request Frontend lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant