The ONE helper script prepended to every widget srcdoc — defines MJWhiteboard.submit
(explicit input path), installs the passive ambient-interaction recorder, AND answers the
host's two-way commands (inspect / operate / call / restoreState / hello — see
whiteboard-widget-protocol.ts). Hand-written ES5 in seven IIFE sections (bridge, author,
inspect, operate, actions, state, commands) that share the helper-private
MJWhiteboard.__h namespace; every handler is try/catch-wrapped.
'*' as the target origin is acceptable here: the sandboxed frame has an opaque origin
(there is nothing else to address), and the HOST validates marker + event.source.
The widget-facing API: MJWhiteboard.submit(data), onAction(name, handler, description?),
offAction(name), setState(obj), onRestore(fn), and
configure({ agentOperable?, autoState? }). data-mj-agent="off" on a control (or an
ancestor) keeps the agent from operating it; data-mj-keep-live on <html> / <body>
turns off automatic form state.
AUTHORSHIP: submit messages and interaction batches carry by: 'agent' when the helper
attributes them to an agent command (PLAN §6.6); absent means the user. It is a CLAIM —
the host applies its own time-window gate before trusting it.
The recorder, in widget-side terms:
click — buttons / links / labels / role=button / aria-labeled elements only
(body/div noise without a label is ignored); records the tag + best label
(aria-label || name || id || trimmed textContent, ≤40 chars).
change — select / radio / checkbox / text fields: field label + new value
(checkbox → 'checked'/'unchecked').
input — debounced per-field (800 ms): field label + current value.
focusin / focusout — field label only.
PRIVACY: type="password" / type="hidden" and protected (card / PIN / OTP / SSN /
account …) fields are skipped entirely; every
recorded value is truncated to 120 chars BEFORE leaving the frame.
BATCHING: events queue and flush via postMessage every 1500 ms or at 12 queued.
RESILIENCE: every handler body is try/catch-wrapped — recorder errors never leak
into widget code.
AUTO-SUBMIT FALLBACK: agent-generated widgets don't always wire
MJWhiteboard.submit — a plain <form> submit would NAVIGATE the sandboxed frame
(blanking the widget) and an inert Submit button goes nowhere. The helper therefore:
intercepts every submit event (preventDefault — the frame never navigates) and
forwards the form's serialized fields as { __auto: true, fields };
watches clicks on submit-ish buttons (type=submit, or labeled submit/send/done/…)
and, shortly after, forwards the nearest form's (or document's) fields the same way.
The submit-ish test reads the type ATTRIBUTE: a typeless <button> reports
type === 'submit' as a PROPERTY, which auto-submitted "Show hint" / "Play" buttons;
DEDUPES: an explicit MJWhiteboard.submit (or a prior auto-forward) within 600 ms
suppresses the fallback, so well-built widgets never double-submit.
Serialization (collectFields) respects the recorder's privacy rules (password/hidden
skipped, values clipped) and only checked radios/checkboxes contribute: text, select
and textarea values as strings, the checked radio as a string, and every ticked
checkbox of a group as an ARRAY of their values under the group's label — one stable
shape whatever the count, so the agent can react to a multi-select answer. A ticked
box or radio with NO value attribute reports its own label text (<label> wrapper
or for=) instead of the native "on", so valueless agent HTML still says WHICH box;
a non-blank value always wins (an empty or whitespace-only attribute counts as
absent), and a box with neither keeps "on".
The ONE helper script prepended to every widget
srcdoc— definesMJWhiteboard.submit(explicit input path), installs the passive ambient-interaction recorder, AND answers the host's two-way commands (inspect / operate / call / restoreState / hello — seewhiteboard-widget-protocol.ts). Hand-written ES5 in seven IIFE sections (bridge, author, inspect, operate, actions, state, commands) that share the helper-privateMJWhiteboard.__hnamespace; every handler is try/catch-wrapped.'*'as the target origin is acceptable here: the sandboxed frame has an opaque origin (there is nothing else to address), and the HOST validates marker +event.source.The widget-facing API:
MJWhiteboard.submit(data),onAction(name, handler, description?),offAction(name),setState(obj),onRestore(fn), andconfigure({ agentOperable?, autoState? }).data-mj-agent="off"on a control (or an ancestor) keeps the agent from operating it;data-mj-keep-liveon<html>/<body>turns off automatic form state.AUTHORSHIP: submit messages and interaction batches carry
by: 'agent'when the helper attributes them to an agent command (PLAN §6.6); absent means the user. It is a CLAIM — the host applies its own time-window gate before trusting it.The recorder, in widget-side terms:
click— buttons / links / labels / role=button / aria-labeled elements only (body/div noise without a label is ignored); records the tag + best label (aria-label || name || id || trimmed textContent, ≤40 chars).change— select / radio / checkbox / text fields: field label + new value (checkbox → 'checked'/'unchecked').input— debounced per-field (800 ms): field label + current value.focusin/focusout— field label only.type="password"/type="hidden"and protected (card / PIN / OTP / SSN / account …) fields are skipped entirely; every recorded value is truncated to 120 chars BEFORE leaving the frame.AUTO-SUBMIT FALLBACK: agent-generated widgets don't always wire
MJWhiteboard.submit— a plain<form>submit would NAVIGATE the sandboxed frame (blanking the widget) and an inert Submit button goes nowhere. The helper therefore:submitevent (preventDefault — the frame never navigates) and forwards the form's serialized fields as{ __auto: true, fields };<button>reportstype === 'submit'as a PROPERTY, which auto-submitted "Show hint" / "Play" buttons;MJWhiteboard.submit(or a prior auto-forward) within 600 ms suppresses the fallback, so well-built widgets never double-submit.collectFields) respects the recorder's privacy rules (password/hidden skipped, values clipped) and only checked radios/checkboxes contribute: text, select and textarea values as strings, the checked radio as a string, and every ticked checkbox of a group as an ARRAY of their values under the group's label — one stable shape whatever the count, so the agent can react to a multi-select answer. A ticked box or radio with NOvalueattribute reports its own label text (<label>wrapper orfor=) instead of the native "on", so valueless agent HTML still says WHICH box; a non-blankvaluealways wins (an empty or whitespace-only attribute counts as absent), and a box with neither keeps "on".