Skip to content

feat(studio): let an agent edit text and styles, guarded - #3518

Draft
miguel-heygen wants to merge 1 commit into
feat/studio-webmcp-inspectfrom
feat/studio-webmcp-content
Draft

feat(studio): let an agent edit text and styles, guarded#3518
miguel-heygen wants to merge 1 commit into
feat/studio-webmcp-inspectfrom
feat/studio-webmcp-content

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

studio_set_text and studio_set_style, the first tools that change the composition.

Also plumbs the write-blocked state, which was the blocker for shipping any write at all.

Stacked on #3517.

Why

Four read tools shipped before this one on purpose. An agent that can see the composition but not touch it is only half useful, but a tool that writes without a guard is worse than none, because Studio has several paths where a failed write reports success.

How

Both act on the current selection and take no handle. That is forced rather than chosen: the handlers read the ambient React selection, and applyDomSelection only schedules a state update, so selecting and committing inside one call would write to whatever was selected before. Select first, then edit.

The write guard. domEditSaveQueuePaused and the external-file conflict both lived on App and were unreachable from the tool surface, so canWrite was optimistic and the code said so in a comment. They now derive into a single writeBlockedReason on the shell context: one field, one owner, conflict taking precedence because resolving it is what unblocks the queue.

That guard matters more than it looks. Both states are banners in Studio with no lock behind them, so nothing else was stopping a programmatic write from landing on top of a conflict the user had been asked to adjudicate.

Three things the tools refuse to fake:

They check the outcome, not the absence of a throw. Awaiting the handler proves nothing here; the tagged outcome added in #3510 is what proves the write landed.

A partial style result is reported as partial. handleDomStyleCommit is one property per call, so N properties are N commits. The result carries applied and rejected maps rather than one boolean that would have to pick a side.

Style commits run sequentially, never concurrently. Two commits racing through Studio's client-side read-modify-write can record undo entries that both claim the same starting content.

Every decline reason maps to a hint naming what to do instead, so a refusal routes the agent rather than just stopping it.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)

14 tests in contentTools.test.ts:

  • Text writes and reports the result, including changed: false when it already said that.
  • A blocked write never reaches the handler, asserted for both the conflict and paused-queue reasons.
  • A declined commit is not reported as success, which is the whole point of the outcome plumbing.
  • Decline reasons map to hints naming the alternative tool.
  • Concurrency is measured, not assumed: the sequential-commit test tracks in-flight count and asserts it never exceeds 1.
  • Partial style success reports applied and rejected separately.
  • Empty and non-object styles are rejected without committing.

Full package suite 4577 passing across 412 files. bunx tsc --noEmit, bunx oxlint and bunx fallow audit --fail-on-issues all clean.

Still no browser verification anywhere in this stack. That is the final unit.

Note

The push for this branch raced a full disk; the commit and remote ref landed, the local upstream config did not and was repaired afterwards. Nothing in the diff is affected.

The first tools that change the composition. Both act on the current
selection and take no handle, which is forced rather than chosen: the
handlers read the ambient React selection, and `applyDomSelection` only
schedules a state update, so selecting and committing inside one call would
write to whatever was selected before. Select first, then edit.

Also plumbs the write-blocked state, which was the blocker for shipping any
write at all. `domEditSaveQueuePaused` and the external-file conflict both
lived on App and were unreachable from the tool surface, so `canWrite` was
optimistic and a comment said so. They now derive into a single
`writeBlockedReason` on the shell context: one field, one owner, conflict
taking precedence because resolving it is what unblocks the queue.

That guard matters more than it looks. Both states are BANNERS in Studio with
no lock behind them, so nothing else was stopping a programmatic write from
landing on top of a conflict the user had been asked to adjudicate.

Three things the tools refuse to fake:

They check the outcome, not the absence of a throw. Studio has several paths
where a failed commit resolves anyway, so awaiting the handler proves nothing.
The tagged outcome added earlier is what proves the write landed.

A partial style result is reported as partial. `handleDomStyleCommit` is one
property per call, so N properties are N commits; the result carries `applied`
and `rejected` maps rather than a single boolean that would have to pick a
side.

Style commits run sequentially, never concurrently. Two commits racing through
Studio's client-side read-modify-write can record undo entries that both claim
the same starting content. There is a test that measures concurrency rather
than trusting the loop.

Every decline reason maps to a hint naming what to do instead, so a refusal
routes the agent rather than just stopping it.
@miguel-heygen
miguel-heygen force-pushed the feat/studio-webmcp-inspect branch from bc098b8 to 1478adf Compare August 27, 2026 04:48
@miguel-heygen
miguel-heygen force-pushed the feat/studio-webmcp-content branch from 953a529 to f766c84 Compare August 27, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant