A composer for AI requests that makes the three parts of an intent visible while you type: the outcome, the constraints (budget, deadline, things to avoid, place, quantity…) and the delegation boundary. Detected spans are highlighted in place, each part with its own underline style, then fly into three slots as editable, removable chips; empty slots offer one-click augmentation prompts and a meter shows how complete the intent is. A three-stop delegation leash (Suggest · Prepare · Act) is set from the verbs you use unless you choose it yourself, spells out what the assistant may do, and morphs the submit button — at Act, executing takes a press-and-hold. Pluggable parser (sync or async, e.g. an LLM); the built-in heuristics understand English and French.
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="ic-demo" style="width:min(100%,54rem);margin-inline:auto">
<style>
#ic-demo .ic-title { display:flex; align-items:baseline; justify-content:space-between; flex-wrap:wrap; gap:.25rem 1rem; margin:0 0 .75rem }
#ic-demo .ic-title h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#ic-demo .ic-title span { color:var(--mv-fg-muted); font-size:.8125rem }
#ic-demo .ic-card { padding:1.125rem 1.25rem 1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#ic-demo .ic-try { display:flex; align-items:center; flex-wrap:wrap; gap:.5rem; margin-top:.875rem }
#ic-demo .ic-try > span { color:var(--mv-fg-muted); font-size:.8125rem; margin-inline-end:.125rem }
#ic-demo .ic-log { min-height:1.25rem; margin:.75rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; text-align:center }
#ic-demo .ic-log strong { color:var(--mv-fg); font-weight:600 }
#ic-demo details { margin-top:.5rem; font-size:.8125rem }
#ic-demo summary { cursor:pointer; color:var(--mv-fg-muted); width:max-content }
#ic-demo pre { max-height:14rem; overflow:auto; margin:.5rem 0 0; padding:.75rem 1rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-muted); color:var(--mv-fg); font:.75rem/1.5 var(--mv-font-mono) }
@media (max-width:30rem) { #ic-demo .ic-card { padding:1rem .875rem } }
</style>
<div class="ic-title"><h3>Travel & work assistant</h3><span>Northwind Consulting · Business travel</span></div>
<div class="ic-card">
<mv-intent-composer id="ic-composer" name="intent" label="What should the assistant do?" hold="1400">Plan my trip to Chicago next Tuesday, budget $400 max, no flights, and book the hotel near Union Station</mv-intent-composer>
</div>
<div class="ic-try" role="group" aria-label="Example requests">
<span>Try:</span>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" data-example="Plan my trip to Chicago next Tuesday, budget $400 max, no flights, and book the hotel near Union Station">Chicago trip</button>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" data-example="Find me three gift ideas for my sister, under $50, delivered by Saturday">Gift ideas</button>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" data-example="Draft an email to the team to move Thursday's meeting, friendly tone, without mentioning the budget">Team email</button>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" data-example="Plan my Chicago trip">Underspecified</button>
</div>
<p class="ic-log" id="ic-log" role="status"></p>
<details>
<summary>Structured intent (JSON)</summary>
<pre id="ic-json"></pre>
</details>
<script type="module">
const c = document.getElementById("ic-composer");
await customElements.whenDefined("mv-intent-composer");
const out = document.getElementById("ic-json");
const log = document.getElementById("ic-log");
const show = () => {
const { text, ...rest } = c.intent;
out.textContent = JSON.stringify(rest, null, 2);
};
c.addEventListener("mv-intent-change", show);
show();
c.addEventListener("mv-intent-submit", (e) => {
const { delegation, constraints, outcome } = e.detail;
log.replaceChildren(
Object.assign(document.createElement("strong"), { textContent: `${delegation.label}` }),
` · “${outcome || "No outcome"}” with ${constraints.length} constraint${constraints.length === 1 ? "" : "s"} sent to the assistant.`,
);
});
for (const b of document.querySelectorAll("#ic-demo [data-example]")) {
b.addEventListener("click", () => {
c.resetDelegation();
c.value = b.dataset.example;
log.textContent = "";
});
}
</script>
</div>
API
Attributes
Name
Type
Default
Description
label
string
Your request
Visible label of the text area.
placeholder
string
Describe the result you want, your constraints, and what the assistant may do on its own…
Placeholder of the text area.
name
string
Form field name (the element is form-associated).
value
string
Initial request text. The element's text content is used when absent.
delegation
suggest | prepare | act
suggest
Level used when the text contains no delegation verb. A detected verb overrides it; a manual choice on the leash overrides both.
hold
number (ms)
1200
Press-and-hold duration required to execute at the Act level (minimum 400).
debounce
number (ms)
280
Pause in typing before the text is analysed again (highlights follow the edit immediately).
rows
number
3
Minimum height in lines; the text area grows with its content.
max-rows
number
10
Height in lines after which the text area scrolls.
submit-as
json | text
json
Submitted form value: a JSON intent { text, outcome, constraints, delegation } or the plain text.
disabled
boolean
Disables the field (also follows a disabled fieldset).
required
boolean
The request cannot be empty (form validation).
data-level
suggest | prepare | act
Reflected: effective delegation level, for styling.
data-analyzing
boolean
Reflected while an asynchronous parser is running.
Properties
Name
Type
Description
parser
(text) => IntentResult | Promise<IntentResult>
Replaces the built-in heuristics (can be set before the element is defined). IntentResult: { outcome: string | { text, start?, end?, spans?: [start, end][], vague? }, constraints: { type, text, label?, start?, end? }[], delegation: "suggest" | "prepare" | "act" | { level, verbs?: { level, text, phrase?, start?, end? }[] }, spans?: { type: "outcome" | "constraint" | "delegation", kind?, start, end }[] }. Offsets are optional: without them there is no highlight or chip editing. Stale async results are dropped.
Effective delegation level. Assigning it is a manual choice; assigning null returns to automatic.
Methods
Name
Description
analyze()
Runs the parser now; resolves with the intent.
insert(template)
Inserts text at the caret; a part wrapped in {braces} is selected afterwards so the user can type over it.
resetDelegation()
Drops the manual choice: the text decides the level again.
submit()
Submits programmatically (no hold required). Resolves to false if cancelled or empty.
focus()
Focuses the text area.
Events
Name
Description
mv-intent-change
After each analysis or delegation change. detail: the intent ({ text, outcome, constraints, delegation, completeness, complete }).
mv-intent-submit
Cancelable. detail: { text, outcome, constraints, delegation }. When not cancelled and the element is inside a form, the form is submitted (requestSubmit).
Content structure
Name
Description
(text)
Initial request as plain text content (rendered before the element upgrades, which also suits SSR).
Text size of the request (16px avoids zoom on iOS).
Accessibility
The highlight layer is aria-hidden; the text area is a native textarea with a visible label and its keyboard hint as description. Each slot is a labelled group with a labelled list; chips are real buttons (Enter or F2 edits in place, Delete removes a constraint, Escape cancels an edit), removals go through the editing pipeline so Ctrl+Z in the text restores them. Ignoring a delegation verb strikes it through (never color alone). A polite live region announces detected parts in batches (“Constraint detected: budget, $400 max”), level changes and the boundary sentence, without interrupting typing. The leash is a role=radiogroup with roving focus (arrows, Home, End) and a described boundary; its mode (auto / manual) is shown as text. The completeness indicator is a role=meter with a spoken summary. At Act, a pointer press-and-hold fills a progress ring; on keyboard, holding Enter or Space works too, and a short press arms a confirm step (Enter again to execute, Escape to cancel); assistive-technology clicks use the same confirm step. Ctrl/⌘ Enter submits from the text (or arms confirmation at Act). Status colors always come with an icon and a label. Reduced motion: no flying chips or label morph; the hold ring still shows progress. No animation loop runs except while holding.