Repository navigation
MIO: Let response buttons write, for a choice after a conflict - #1052
Conversation
…onflict Adds a plan for a narrowly scoped `effect: 'write'` response action that only a trusted user click can run, with choice groups, app-written consequence text, ledger entries, a next-turn history record and a `responseActionEffects` lease property for feature detection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An app's save can fail because the user edited the document while MIO was working. The app can now end that turn with a question and buttons the user answers, instead of an error that loses MIO's work. - `effect: 'write'` on MioResponseAction. It runs only from a trusted click, at most one per message, only on the newest assistant message, and returns a MioOperationOutcome. A throw or malformed outcome is recorded as unknown; closing the chat does not roll a write back. - `group` makes actions on one message a single choice, and `description` carries app-written consequence text below the row. - Writes go through the operations ledger as `response:<id>`, with a new `MioOperation.source`, so inspectOperation reconciles them. - The next turn's history carries a shell-authored record of the choice. - `MioWindowLease.responseActionEffects` lets apps detect support. - `<os-button>` forwards `aria-description` onto its native button. Replaces the proposal document added earlier on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude can you review this new addition to the MIO framework? |
|
Claude finished @AllTerrainDeveloper's task in 1m 3s —— View job Review: MIO write response actionsI read the full The design holds together. The Worth fixing or deciding
Smaller notes
Rulebook checks
Not tested in a browser (as your description says). I'd check two things by hand: a keyboard Enter/Space press on the Apply button, to confirm it counts as trusted, and the retired state after a new message. |
- Keep the record of a user's choice when the next turn fails or is stopped before the provider answers, so a later turn still reports it. - A new user message retires every action in a write's choice group, not only the write, so "Keep" can't answer a decision that has passed. - One description per message, announced by whichever button owns it, and withdrawn once that action has settled. - One history record per action: a retried write reports its latest outcome only. - `MioOperation.source` is optional in the type, so app-built records stay valid; the ledger always sets it. - Match MIO's "Could not …" wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
epeicher
left a comment
There was a problem hiding this comment.
I have reviewed the changes and they LGTM!
Why
An app's save can fail because the user edited the document while MIO was working. For example, someone edits a form field while MIO is validating a new version of that form. Until now the turn could only end in an error, and MIO's work was lost. With this PR the app can end the turn with a question and buttons instead:
Before this change the shell dropped any response action that wasn't
readornavigate. This PR adds a narrowly scopedwriteaction that only a person can trigger. Every change is additive and optional.What changes
effect: 'write'onMioResponseAction(src/mio/assistant/response-actions.ts)event.isTrustedis true. A programmatic call, a synthetic event, a render pass orallowed()cannot run it.MioOperationOutcome:confirmed: the button shows "Done." and retires.unknown: the button retires and says to check the result before retrying.rejected: the first error is shown. Whenretryableis true, another click retries with the sameidempotencyKeyand a newcallId.unknown.unknown.group: actions on one message that share a group are one choice. While one is pending the others are disabled. When one finishes (a confirmed write, or a read that resolves) the choice is final: the others disappear and the chosen one keeps its status. A rejected or unknown write, or a read that throws, leaves the group open.description: plain-text consequence text written by the app, 1–160 characters. A message keeps only one, and any later description is dropped. It's rendered below the row while its action can still be chosen, and the button that owns it announces it as its accessible description, whatever its effect.Operations ledger: each write is recorded as
ability: 'response:<id>'with the turn ID, window ID andrevision(). It firesonOperationwhen it starts and again when it settles, andgetOperations(),inspectOperation()andoperationStatuscover it.MioOperationgainssource?: 'ability' | 'response-action'. The ledger always sets it. It's optional in the type so records that apps build themselves still typecheck, and a missing value means'ability'.Next-turn history: each settled write, and each settled choice in a group, adds one record to the next turn's
outcomes, for example{ source: 'response-action', label, effect, status }. There is one record per action, so a retried write reports only its latest outcome. Each record is reported once, and it goes back into the queue if that next turn fails or is stopped before the provider answers. It contains no callback, ID, URL or receipt body.Feature detection:
MioWindowLease.responseActionEffectsis['read', 'navigate', 'write']. Apps should treat a missing property as['read', 'navigate'].<os-button>forwardsaria-descriptiononto its native button, the same way it already forwardsaria-label. This differs from the original plan. Anaria-describedbyon the host can't reach the shadow<button>, because an IDREF doesn't cross the shadow root (the component docs already note this). Without the forwarding, the description would not be announced.No provider, PHP or transport changes.
Docs
docs/mio-window-assistant.md: neweffect,groupanddescriptionrows, new sections on response buttons that write, choice groups and what the model hears about a choice, andsourcein the ledger description.docs/javascript-reference.md: the new fields, the context additions,MioOperation.sourceandresponseActionEffects.docs/api-index.md: write response actions listed as Experimental.docs/examples/mio-form-editing.md: a new "Choose after a conflict" recipe next to Preview.docs/components-reference.md: thearia-descriptionforwarding.Tests
The new file
tests/vitest/mio-response-actions-write.test.tscovers each acceptance criterion:inspectOperationreconciles an unknown write.Also added: a lease test for
responseActionEffectsand anos-buttonforwarding test. One existing test listedeffect: 'write'as an invalid descriptor, so it now uses an unknown effect.npm run build,npm run lint(0 errors),npm run typecheckandnpm run test:js(544 files, 6927 tests) pass locally. In headless Chromium, Enter, Space and a mouse click on a button inside a shadow root (likeos-button's) all reach the host as trusted clicks, while a scripted.click()does not. So keyboard users can trigger a write and scripts can't. I haven't tried the feature by hand in a browser, or checked Safari or Firefox.Changes after review
prompt()hook throws, the request is over budget, or the user presses Stop), the records go back and the turn after reports them.MioOperation.sourceis optional in the type.The first commit on this branch added the proposal as a plan document. The second commit removes it and implements the proposal, and the third applies the review changes.
🤖 Generated with Claude Code