Changelog
Version 0.11.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
f52dbe9: Refusing a tool no longer ends the turn. The model gets a turn to answer in, so it reads the refusal.
Before: a refusal appended a “Tool execution was rejected by the user.” tool_result and stopped the run — so the one sentence written for the model was never sent to it. Telling the assistant what you actually wanted meant retyping it as a new message, which it then read out of order.
After: the turn’s remaining tool calls are still skipped (the model may have asked for several, and refusing one cannot license the others — that part was a real fix and it stands), and then another turn runs.
This needed one flag to become three states, because a refusal answers two questions differently: run the calls that follow this one? — no; take another turn? — yes. Core’s
_handleToolUseEventreturns'continue' | 'respond' | 'halt'; the engine’s loopbreaks without clearingcontinueLoop. Both changed together, and the parity suite stayed green through it — which is the suite doing its job: it asserts the two loops agree, never what they do. The scenario named “rejected stops the loop identically” had to be renamed by hand for exactly that reason.Two other outcomes are now visibly distinct from a refusal rather than sharing its exit: a per-tool turn limit, a missing handler, and an abort all
haltand tell the model nothing.If you depended on the old behaviour — a refusal ending the run — refuse from an
approvalResolverand stop the client yourself, or setmaxTurns: 1on the tool.@aparte/core,@aparte/engine -
e40cf78: Breaking, pre-1.0, no shim: a request for the human that ends without an answer now rejects instead of resolving
{ action: 'cancel' }.AparteElicitationResultloses itscancelarm and keepsaccept/decline. The failure arrives as the newAparteElicitationAbortError, whosenameis'AbortError'— so any handler already testingerr.name === 'AbortError'needs no change — and whosereasonis'aborted'(a stopped turn, a fired signal, a question taken away by another request) or'no-presenter'(nothing was mounted to ask it).Why the shape had to change: a value is easy to handle as though it were an answer, and that is exactly what happened one level up. The tool-approval gate read
cancelas a refusal, stamped the segmentrejected, and told the model “Tool execution was rejected by the user.” The user had pressed Stop. A rejection cannot be mistaken for a decision by a caller that forgot a branch, which is the propertycancelnever had.Evidence the shape is right:
askUserHandleralready performed this exact conversion by hand —{ action: 'cancel' }in,new DOMException(..., 'AbortError')out. That conversion is gone; the error now propagates from the primitive.Migrating. Replace a
case 'cancel':branch with acatch. Aswitchonactionthat had all three arms keeps compiling with two, and the third path becomes thecatch. One consequence worth knowing: a request you start and neverawaitwill surface an unhandled rejection when it ends without an answer, because that is what an ignored failed promise is — attach a.catch()if you genuinely do not care about the outcome.@aparte/core -
ecd9ad5: A tool call now shows what went in and what came out, and it is drawn as a row rather than a badge.
The pill named the tool and showed nothing else — not the arguments the model chose, not the result it got — while the segment carried both the whole time. Missing presentation, not missing data. It opens onto
Input(pretty-printed JSON) andOutput, coloured by a registered highlight provider when there is one and readable as escaped text when there is not.Collapsed, always — including while the loop waits for a decision. The reasoning block stays closed while it is being produced, which is the most live moment there is, so a tool call has no stronger claim to unroll itself. One rule, no special cases. A
<details>appears only when there is something behind it: a disclosure onto nothing is an affordance that lies.Breaking, pre-1.0: four CSS classes are renamed, because a name in a public CSS contract must name a ROLE and not a shape — the shape belongs to whoever is styling it.
tool-pill→tool-label,tool-pill-icon→tool-icon,tool-pill-name→tool-name,tool-pill-spinner→tool-spinner,tool-pill-status→tool-state. Same reasoning that retiredfooter-left/center/right: a name the design contradicts is a name that will lie.And it no longer looks like a tag. The identity is neutral at every status — it used to be filled green when a call resolved and red when it was refused, which made a finished step shout louder than the reply it belongs to. The colour lives on a small state badge at the far end, which now carries a WORD as well as a glyph (
Running,Done,Rejected,Stopped): a bare cross beside a name reads as a button that removes something, so the state was being mistaken for an affordance.The renderer gains an
update, which it never had. Without one the bubble replaced the element on every change — and a tool call changes status several times a turn, so a disclosure the reader opened would have slammed shut under them each time. A registeredregisterToolRendererstill owns its whole markup and is rebuilt rather than patched.New locale keys:
toolInput,toolOutput,toolRunning,toolCompleted,toolRejected,toolStopped, translated in@aparte/locale-fr. New themable variable:--aparte-tool-row-radius.@aparte/core -
56e1247: An open request now follows a language switch.
AparteElicitationRequest.messageandAparteApprovalOption.labelacceptstring | (() => string); the function arm is re-read whenever the locale changes while the request is on screen.Additive — a string still behaves exactly as before, and deliberately so: a plain string is treated as the host’s own wording and left alone. That is right for an app’s text and wrong for locale-derived text, which is why core’s own approval gate now passes functions.
The gate was asking
Run delete_file?over buttons readingApprouverandRejeter.approveToolandrejectToolhave been translated in@aparte/locale-frsince long before this: nothing was missing from the translations, the re-read path was missing. It existed while the buttons lived in the segment, and moving them to the composer left it behind.The tool’s NAME is substituted into the question and never translated — it is the identifier the model called, wire format, so only the frame switches.
@aparte/core -
094d438: The tool-approval decision moves out of the transcript and into the composer.
A request that blocks the run is answered where the user answers. That is now a rule for the library, not a choice made once: the composer is where a question already went, and the approval gate was the only decision surface left in a bubble. It was older than the mechanism that should have carried it — built with a segment renderer and a
documentevent because neithershowPanelnor a typed presenter existed yet — and nothing came back for it, partly because for a stretch the whole human-in-the-loop path was inert and so nothing exercised it.What you see. The
tool_callpill stays in the transcript as the anchor, saying which tool is waiting, with no role, no tab stop and nothing clickable. The choices appear in the composer, each settling on the first click, above a quiet field for saying what to do instead. The thing being judged stays in the thread, which is scrollable, copyable and persisted; the panel is capped at half the viewport and could not hold a diff or a plan.Breaking, pre-1.0, no shims:
aparte-tool-decisionis deleted — the event,AparteToolDecisionDetail, its event-map entry and thedocumentlistener that answered it. It existed only because a segment renderer has no reference to the client. To answer programmatically, pass anapprovalResolveror register your own presenter; both see the whole request instead of an id on an event.AparteToolApprovalResolverandStreamApprovalResolvertake the CALL,(call, signal)rather than(toolCallId, signal). You cannot ask a person “run this?” without naming what — and an id alone forced a lookup table filled by one event and read by another, the shape that breaks in silence.- Both resolvers may return an
instruction, the words the model reads back on a refusal. AparteElicitationRequestgainskindandoptions, andschemais now optional — required on a'question', absent on an'approval'.
New:
buildApprovalPanelandBuiltApprovalPanel,AparteApprovalOptionandAparteApprovalAnswer, and four locale keys (approvalAsk,approvalWaiting,approvalInstructionPlaceholder,approvalOptionsLabel), translated in@aparte/locale-fr.<aparte-chat>now ships<aparte-elicitation>in its default composition. The built-in gate asks through the presenter, so a chat without one could not honourneedsApprovalat all. An affordance core honours end to end is on by default; leaving this out would have made the gate depend on a tag nobody was told to write. Author-provided compositions are untouched, as always.The options come with the request. Core supplies two — the tool’s name as the question, Approve and Reject — and anything richer is the host’s: a scope option (“and always for this tool”) exists only because an app wrote the label and can remember the grant. Core never invents one and never interprets one.
Also fixed, in passing: a panel’s own buttons inherited the composer row’s 44×44 action-control sizing and rendered as circles with their labels spilling out. Any panel containing a button hit this; the approval options were the first that do.
@aparte/core,@aparte/engine -
5ac31ff: Every CSS class core emits is now prefixed
aparte-. Breaking, pre-1.0, no aliases: 42 names across 291 occurrences.aparte-segmentaparte-segment-contentaparte-segment-textaparte-segment-thinkingaparte-segment-codeaparte-segment-erroraparte-segment-tool-callaparte-segment-artifact-cardaparte-segment-artifact-fileaparte-segment-pipeline-waitingaparte-segment-unknown·aparte-tool-summaryaparte-tool-toggleaparte-tool-labelaparte-tool-iconaparte-tool-nameaparte-tool-spinneraparte-tool-stateaparte-tool-detailaparte-tool-partaparte-tool-part-labelaparte-tool-part-body·aparte-code-content-wrapperaparte-code-copyaparte-code-filenameaparte-code-headeraparte-code-header-filleraparte-code-language·aparte-error-contentaparte-error-detailsaparte-error-icon-wrapperaparte-error-messageaparte-error-title·aparte-thinking-contentaparte-thinking-headeraparte-thinking-labelaparte-thinking-toggle·aparte-is-streamingaparte-is-focusedaparte-is-dragoveraparte-has-content·aparte-pw-dotIf you style any of these, add the prefix.
--aparte-*custom properties are unchanged — they were already namespaced.Why it mattered in both directions. Core is light DOM on purpose: no shadow root, so every selector reaches in and out. Inbound has bitten this project twice already — a bare
nav { justify-content: space-between }on aparté’s own docs site pushed the artifact card’s tabs to opposite ends, and.segmentis Semantic UI’s base layout class. Outbound is the worse half and was never stated: these were bare global selectors, so@aparte/coreshipped a rule for.error-message,.code-headerand.thinking-headeronto the whole page. Almost every site has an.error-message.The component classes were already prefixed (
aparte-message,aparte-composer-row,aparte-approval-option,aparte-elic-panel); the renderer classes never were. With no written policy, the split held at 146 to 42. The policy is now in CLAUDE.md.One deliberate exception:
language-*on a code block stays unprefixed, because that is the class name highlighters look for.Removing the
progresssegment in the same release already took outprogress-barandprogress-fill, which are Bootstrap’s.@aparte/core -
c4d87a2: A second request for the human now waits instead of being answered
cancelon arrival.AparteConfig.requestUserInputholds a queue, so one request reaches the presenter at a time. That limit is real — the composer has one panel slot, and a second request used to clobber the first’s DOM — but the old protection lived in<aparte-elicitation>, which resolved the second request{ action: 'cancel' }immediately. That is a refusal invented for a question nobody was ever shown, and the model reads it as the user having refused. Waiting is the honest behaviour.Two things this also fixes: a consumer’s own presenter, registered with
setElicitationPresenter, previously had no protection at all; and a request that has been queued while its turn is stopped is no longer presented, because asking about a run that is already over asks about nothing.Filed minor rather than patch for one reason worth naming: code that leaves a request unawaited and then awaits a second one used to get an immediate
canceland now waits for the first to settle. Nothing in this repo did that, and a dangling request is itself settled by the composer’s turn-end eviction, but the shape of the change is visible enough to be a minor.The queue only costs a microtask when something is actually ahead: with nothing waiting, a request is still presented in the calling tick, which is what the panel being mounted synchronously depends on.
@aparte/core -
c6d3a20: The
progresssegment is removed.AparteProgressSegment,progressRenderer, its registration inregisterDefaultRenderers(), its CSS and its three--aparte-progress-*variables all go. Breaking, pre-1.0, with no alias and no shim.No language model emits a progress bar. Not chat-completions, not Anthropic’s messages API, not the AI SDK’s stream protocol — a model emits text, reasoning, tool calls, tool results and sometimes citations. And nothing in this repo emitted one either: the only in-repo
'progress'is a worker→main message in@aparte/provider-transformersreporting model download progress to anonProgresscallback, which is a name collision and never a segment.label+percent+statusare the signature of an app that owns the work — word for word the reason theterminalsegment was removed, and the sixth segment type to go for it. The line it sits on the wrong side of is visible one file away:pipeline-waitingstays, because core emits that one itself between the phases of a multi-step turn. Core-owned indicator, not app-owned data.An app that wants a progress bar has the seam for it:
registerSegmentRendererwith a segment type of its own. That is the same answer this library gives for a terminal, and it is a better one than a built-in nothing fills.Also fixes the landing’s hero, which claimed “ten kinds of content” over a list of eight. The count is computed from the list now, so it cannot drift again; it reads seven.
@aparte/core -
9e30879: Every aparté element now has a typed surface in all four frameworks.
Placing an element used to mean one of two things: a stringly-typed proxy, or nothing at all. In Angular it was
<aparte-ui name="aparte-model-selector" [props]="{…}" (elementEvent)="…">— a tag name as a string, an untyped bag of props mixing DOM attributes with CSS variables, one output for every event, and an element created imperatively so no@if,@foror content projection could reach it. In React it was nine tags declaredany.@aparte/corenow declares each element’s attributes once —AparteElementAttributes, keyed byAparteElementTagName, with a per-element interface exported for each. Every wrapper derives from that registry rather than listing tags, so an element added to core is typed everywhere the moment it lands.-
React — the
aparte-*JSX intrinsics are typed. A wrong value type is a compile error.Amended after release: this said “a typo, a wrong value type, or an attribute the element does not observe is a compile error”. The value half holds; the other two do not, for the hyphenated names the sentence used as its own examples. TypeScript treats a JSX attribute whose name is not a valid JS identifier as “known” even when it is absent from the attributes type, so none of the 12 hyphenated aparté attributes is typo-checked —
max-rendered-bubles={200}compiles. Presence-attribute enforcement is unaffected. -
Vue — declared through
GlobalComponents, checked byvue-tsc. -
Svelte — declared through
SvelteHTMLElements, checked bysvelte-check, includingon:handlers derived from the DOM event map. -
Angular — a standalone directive per element, exported individually and as
APARTE_ELEMENT_DIRECTIVES. Real@Input()s that write attributes (never properties — eight of<aparte-composer>’s accessors are getter-only), one typed@Output()per event emitting the event’s detail, and the real tag in the template so control flow and projection work. It also means noCUSTOM_ELEMENTS_SCHEMA, which used to switch template checking off for every unknown tag in the file.
In the three template languages a presence attribute is
'' | null | undefined, notboolean: all three stringify what they set on a custom element, sosearchable={false}would rendersearchable="false"and an element testinghasAttributereads that as on. Angular’s directives take a realbooleanand write the attribute themselves.AparteTemplateAttrsandAparteAttrValueare exported if you build your own integration.<aparte-ui>is unchanged and still ships. It is the escape hatch for an element aparté does not define — one of yours, or a third party’s — rather than the way to use aparté’s own.Also fixed while typing it, all found by the compiler rather than by reading: six attributes were documented as strings while the element treats them as booleans or numbers;
timestampaccepts a number as well as a string;framework-managedis a real attribute of the contract that all four wrappers set, core reads on two elements, and nothing declared; andmax-messagesis marked deprecated in favour ofmax-rendered-bubbles, which the element has been warning about at runtime.New docs page: Placing elements, typed.
@aparte/core,@aparte/angular,@aparte/react,@aparte/svelte,@aparte/vue -
-
2f6180e: A wrapper types only what it depends on.
AparteModelSelectorDirectiveandAparteAskUserDirectiveare removed from@aparte/angular;aparte-model-selectorandaparte-ask-userare removed from core’sAparteElementAttributesregistry, along with theAparteModelSelectorAttributesexport; andaparte-model-changeis removed fromAPARTE_DEFAULT_UI_EVENTS.They were added hours earlier in the same release, and the reason to take them back out is the one that matters: a third-party plugin’s author cannot add a line to
@aparte/core. Typing our own plugin’s element from core and shipping its directive from the wrapper gave aparté’s packages a privilege nobody else’s plugin could have — an asymmetry baked into the library before it has an ecosystem.The rule that replaces it is symmetric and states in one line: whoever owns the element owns its contract and its bindings. Core’s elements are typed by core and wrapped by the wrappers. Everything else — a plugin’s element, ours or yours — is typed by its owner, or in six lines by the app that places it. Both mechanisms are documented, and both are exactly the same work for us as for anyone: module augmentation for React/Vue/Svelte (types only, no runtime, applies exactly when the package is installed) and a directive for Angular, whose only non-obvious part — attribute versus property — is
applyElementProps, already exported.Nothing about core’s own 18 elements changes: their attributes, the 26 declared events with 20 typed details, the JSX/Vue/Svelte typing and the 17 Angular directives all stay.
The Angular example keeps its
CUSTOM_ELEMENTS_SCHEMAremoved.Amended after release: this said the example “now declares its own six-line directive for the model selector instead of importing one”. It does not — it imports the directive from
@aparte/plugin-model-selector/angular. The plugin shipped that subpath later in the same release and the example was switched back to it, but this paragraph was not re-measured before publishing. The pattern itself is documented, with a worked example, at/frameworks/elements/#your-own-element-or-a-plugins.@aparte/core,@aparte/angular -
16bcd8a:
@aparte/plugin-model-selector/angular— the fourth and last binding, so all four frameworks now get<aparte-model-selector>from the package that owns it.import { AparteModelSelectorDirective } from "@aparte/plugin-model-selector/angular";// @Component({ imports: [AparteModelSelectorDirective], … })Angular is the only one of the four that needs real code — its template compiler requires a class claiming the selector, and
[persist]="true"on a custom element writes a property, which on an attribute-driven element is a silent no-op. So this entry is compiled in partial-Ivy mode byngc, the format a consumer’s own AOT build finishes, while Vite keeps building everything else. The directive itself is generated from the package’s own custom-elements manifest, like the other three bindings.@angular/coreis an optional peer dependency: install the plugin without Angular and nothing here is reachable, which is the point.The Angular example now imports this instead of the six-line local directive it wrote while waiting — the import resolving at all is the property, since you get the binding exactly when you have the plugin. That local directive remains the documented path for an element aparté does not define.
@aparte/plugin-model-selector -
d03b212:
@aparte/plugin-model-selectortypes its own element, through three new subpath exports:./react,./vueand./svelte.import "@aparte/plugin-model-selector/react";// <aparte-model-selector persist="" searchable="" placeholder="Pick a model" /> ← typedThis is the rule from the previous release made real: whoever owns the element owns its contract and its bindings.
@aparte/angularbriefly shipped a directive for this element and core briefly typed it — both were removed, because a third-party plugin’s author cannot add a line to either, so doing it for our own plugin gave aparté’s packages a privilege theirs could never have.Putting the bindings in the plugin makes the property you actually want fall out of the module graph: install the package and the tag is typed; don’t and it isn’t. TypeScript enforces that, nobody has to remember it.
Subpaths rather than the main entry because a
declare module 'react'block only compiles where React’s types resolve — in a shared entry it breaks every Vue and Svelte consumer withTS2664.react,vueandsvelteare optional peer dependencies; the three modules carry no runtime at all (0.04 kB each, the augmentation is the whole payload).The package now also emits its own custom-elements manifest, and its attribute types are generated from it by the same
scripts/gen-element-bindings.mjsthat generates core’s — so the types cannot fall behind the element’s JSDoc, and a third-party plugin can run the same tool on its own manifest.No Angular subpath yet: an Angular directive is runtime code, so it needs partial-Ivy compilation in a package that builds with Vite. Until then, the six-line local directive the Angular example demonstrates is the path.
@aparte/plugin-model-selector
Patch Changes
-
7336ae4: A built-in renderer’s CSS moved out of
getStyles()and intostyles/aparte.css— the tool call, the artifact card and the pipeline-waiting segment, 425 lines out of the three renderers and 449 into the stylesheet (the difference is section comments and blank lines a real stylesheet gets to have). No visual change: the same rules, in a file that ships the same way.getStyles()stays on the renderer interface, because that seam is what a consumer’s renderer needs — something registered throughregisterSegmentRendererorregisterToolRenderercannot edit core’s stylesheet and has no other way onto the page. A built-in has the stylesheet.Two measured reasons.
check:derived-varsreads that one path and nothing else, so a declaration deriving from another variable could hide in a renderer unchecked. And CSS in a template literal is not read as CSS: a backtick closes the literal — the artifact card’s own comment recorded that happening, and it happened three more times in one sitting, the worst rendering a source marker into an assistant’s bubble as prose, because inside a template literal a//comment is just text.Also removes a dead rule that tinted the tool row’s border while a decision was pending: it stopped painting anything when that border went away in the row redesign, and it reached for
--aparte-border-strong, a variable that was never declared anywhere.Contract-neutral: core’s entry imports the stylesheet and
package.jsonmarks every.cssa side effect, so importing@aparte/corehas always brought it along.@aparte/core -
02f2d4d: The composer’s one panel slot now has an owner, which closes a defect that could permanently stop a chat from asking anything.
showPanelreturns a token and accepts anonEvictcallback;hidePanel(token)closes the panel only if that token still owns the slot. Both additions are additive — code that callsshowPanel()andhidePanel()as before is unchanged.The defect: the composer tears its panel down on every turn-ending event, and
<aparte-elicitation>only listened foraparte-message-errorandaparte-message-aborted. A question still open when a turn completed normally therefore lost its panel while the presenter kept its pending state — sorequestUserInput()never settled, and because the presenter refuses a second request while one is pending, every later question was short-circuited for the life of the page. One finished turn and the chat could never ask again.Three paths could close a panel whose owner was still awaiting an answer, and none of them told the owner: a second
showPanel, the owner’s own latehidePanel, and the turn-end teardown. All three now notify, and a presenter settling late can no longer tear down the panel that replaced its own.@aparte/core -
093a196: The element bindings are generated from the manifest, not written by hand. 675 lines of hand-maintained declarations out — 435 of Angular directives and 240 of attribute interfaces — for a 335-line generator and a 55-line config file, which three packages now share.
Core’s attribute registry and the 17 Angular directives were a parallel structure over facts the custom-elements manifest already carried, with nothing watching them. Add an attribute to an element and the manifest records it, the registry records it, and React, Vue and Svelte type it automatically — they derive from the registry through a mapped type. Angular would silently not, because an
@Input()is a hand-written member. Nothing would go red, and the Angular wrapper would be quietly behind within days.scripts/gen-element-bindings.mjsnow emits both fromdist/custom-elements.json, into gitignoredsrc/generated/directories rewritten on every build — the same pattern the docs’ two generated reference pages already use, so there is no committed artifact that can fall behind and no new guard.The generator was checked differentially against the output it replaced, and reproduced it: the same 15 interfaces carrying the same 48 attribute members, the same 17 directives, the same 24 Outputs. It differs in exactly one place — 41 Inputs where the hand-written directives had 40, because it picked up
framework-managedon<aparte-chat-viewport>, an attribute core’s registry declared and the hand-written directive had missed. That is the drift this change exists to make impossible, found in the artefact being deleted. The 109 directive tests pass unchanged against the generated file.What cannot be derived lives in
packages/core/element-bindings.config.mjs, visible rather than buried in a generator branch:roleon the bubble is omitted as an Input because that name is ARIA’s,data-emptyon the toolbar is omitted because the element reflects it onto itself, andaparte-abort/aparte-message-abortedget no Output because they are dispatched onwindowwhere a host listener could never hear them.No public API changes: the same types and the same directive names are exported, from a generated file instead of a hand-written one.
@aparte/core,@aparte/angular -
e406a98: Every element now declares and describes its own surface, and the generated API reference prints each event’s detail type.
The manifest is the source of truth for the component API, and it was quietly incomplete. Four elements carried a full
@element/@attr/@firesblock at the top of their file, separated from the class by imports and interfaces — TypeScript associates only the comment physically adjacent to a declaration, so every authored description was dropped on the floor. Nothing looked missing: the analyser readsobservedAttributesandthis.dispatchEventstructurally, so<aparte-select>still listed six attributes and three events. They just had no text, and the reference page shipped rows like| aparte-cancel | |.Seven event names reached the manifest through neither path and are now declared by hand, because no docblock fix can make them detectable: the analyser’s fallback only visits real method declarations and only recognises
this.dispatchEvent.<aparte-conversation-list>had no events at all — all four of its dispatches happen in an arrow class field.<aparte-chat-bubble>was missing exactly one,aparte-branch-navigate, for the same reason.<aparte-composer>was missingaparte-abortandaparte-message-aborted, which go out onwindow.Every event that carries a detail now names its type —
@fires {CustomEvent<AparteConversationSelectDetail>} …— sourced fromevent-map.ts, which is guarded in both directions. Before this, all 26 events in the manifest read as a bareCustomEvent; there was no working typed instance in the repo. The generated reference gained a Type column to print it, because that is what tells a consumer the shape ofe.detail.Result: 18 elements, every one with a description, every attribute and event described, 26 events of which 20 carry a typed detail.
@aparte/core,@aparte/plugin-ask-user,@aparte/plugin-model-selector -
6f262cf: Three fixes to the human-in-the-loop gate. No API changes: nothing that compiles today stops compiling.
A stop is no longer reported to the model as a refusal. Pressing Stop while a tool waited for approval stamped the segment
rejectedand put “Tool execution was rejected by the user.” into the history — the sentence the model reads named a decision nobody made. The abort path resolved{ approved: false }, the same value an explicit Reject produces, so the gate could not tell them apart. It now asks the signal instead of the value, and an aborted wait stampsabortedand appends notool_result: there is nothing true to tell the model, which is already how a handler aborted mid-run is treated.A
needsApprovaltool with noapprovalResolveraborts instead of inventing a refusal.runStreamAgentdefaulted toasync () => ({ approved: false }), so a host that had simply forgotten to wire a resolver was reported to the model as having refused.A reloaded conversation stops waiting for a decision nobody can give. A
tool_callpersisted asawaiting-approvalcame back still awaiting it, with Approve / Reject buttons wired to a listener that went with the page — andisSegmentSettledreads status for a tool call, so the segment also stayed open and collected anendedAtfrom the next turn-close.adoptSegmentnow normalises it toabortedon every load path: nobody refused it, the page simply went away. The persistence guide documented this as something core could not fix for you; that half of the paragraph is gone, andpending— the same defect on the sibling nobody had looked at — is named as still outstanding.AparteToolDecisionDetail.targetIdis declared. The runtime always sent it and a test read it, so reaching the chat id on a public event required casting past its own type.@aparte/core,@aparte/engine -
d85cf6b:
APARTE_DEFAULT_UI_EVENTSnow lists every event an aparté element dispatches on itself — 23 names, up from 7.This is the set
AparteUiforwards in all four wrappers, so an event missing from it is an event a consumer cannot hear through the proxy. It described itself as “verified against core” while carrying seven of twenty-three, and the gap was not academic:aparte-model-changewas absent, and<aparte-ui name="aparte-model-selector">was the one worked example in the wrappers’ own documentation — the documented usage could not receive the event it exists to receive.That example is gone from this release for a better reason than a longer list:
@aparte/plugin-model-selectornow types its own element and ships its own bindings, so its event is typed through the DOM and the proxy is not on the path at all.aparte-model-changeis therefore not in this list — a plugin’s event is the plugin’s to declare, and core listing it was the same privilege the boundary change removed everywhere else.Two of core’s own events are also deliberately excluded:
aparte-abortandaparte-message-abortedgo out throughwindow.dispatchEvent, so an element-level listener can never receive them and listing them would promise a forward that cannot happen. That is the whole difference between the manifest’s 25 distinct event names and this list’s 23.@aparte/core -
e40cf78: Breaking, pre-1.0, no shim: a request for the human that ends without an answer now rejects instead of resolving
{ action: 'cancel' }.AparteElicitationResultloses itscancelarm and keepsaccept/decline. The failure arrives as the newAparteElicitationAbortError, whosenameis'AbortError'— so any handler already testingerr.name === 'AbortError'needs no change — and whosereasonis'aborted'(a stopped turn, a fired signal, a question taken away by another request) or'no-presenter'(nothing was mounted to ask it).Why the shape had to change: a value is easy to handle as though it were an answer, and that is exactly what happened one level up. The tool-approval gate read
cancelas a refusal, stamped the segmentrejected, and told the model “Tool execution was rejected by the user.” The user had pressed Stop. A rejection cannot be mistaken for a decision by a caller that forgot a branch, which is the propertycancelnever had.Evidence the shape is right:
askUserHandleralready performed this exact conversion by hand —{ action: 'cancel' }in,new DOMException(..., 'AbortError')out. That conversion is gone; the error now propagates from the primitive.Migrating. Replace a
case 'cancel':branch with acatch. Aswitchonactionthat had all three arms keeps compiling with two, and the third path becomes thecatch. One consequence worth knowing: a request you start and neverawaitwill surface an unhandled rejection when it ends without an answer, because that is what an ignored failed promise is — attach a.catch()if you genuinely do not care about the outcome.@aparte/plugin-ask-user -
ecd9ad5: A tool call now shows what went in and what came out, and it is drawn as a row rather than a badge.
The pill named the tool and showed nothing else — not the arguments the model chose, not the result it got — while the segment carried both the whole time. Missing presentation, not missing data. It opens onto
Input(pretty-printed JSON) andOutput, coloured by a registered highlight provider when there is one and readable as escaped text when there is not.Collapsed, always — including while the loop waits for a decision. The reasoning block stays closed while it is being produced, which is the most live moment there is, so a tool call has no stronger claim to unroll itself. One rule, no special cases. A
<details>appears only when there is something behind it: a disclosure onto nothing is an affordance that lies.Breaking, pre-1.0: four CSS classes are renamed, because a name in a public CSS contract must name a ROLE and not a shape — the shape belongs to whoever is styling it.
tool-pill→tool-label,tool-pill-icon→tool-icon,tool-pill-name→tool-name,tool-pill-spinner→tool-spinner,tool-pill-status→tool-state. Same reasoning that retiredfooter-left/center/right: a name the design contradicts is a name that will lie.And it no longer looks like a tag. The identity is neutral at every status — it used to be filled green when a call resolved and red when it was refused, which made a finished step shout louder than the reply it belongs to. The colour lives on a small state badge at the far end, which now carries a WORD as well as a glyph (
Running,Done,Rejected,Stopped): a bare cross beside a name reads as a button that removes something, so the state was being mistaken for an affordance.The renderer gains an
update, which it never had. Without one the bubble replaced the element on every change — and a tool call changes status several times a turn, so a disclosure the reader opened would have slammed shut under them each time. A registeredregisterToolRendererstill owns its whole markup and is rebuilt rather than patched.New locale keys:
toolInput,toolOutput,toolRunning,toolCompleted,toolRejected,toolStopped, translated in@aparte/locale-fr. New themable variable:--aparte-tool-row-radius.@aparte/locale-fr -
094d438: The tool-approval decision moves out of the transcript and into the composer.
A request that blocks the run is answered where the user answers. That is now a rule for the library, not a choice made once: the composer is where a question already went, and the approval gate was the only decision surface left in a bubble. It was older than the mechanism that should have carried it — built with a segment renderer and a
documentevent because neithershowPanelnor a typed presenter existed yet — and nothing came back for it, partly because for a stretch the whole human-in-the-loop path was inert and so nothing exercised it.What you see. The
tool_callpill stays in the transcript as the anchor, saying which tool is waiting, with no role, no tab stop and nothing clickable. The choices appear in the composer, each settling on the first click, above a quiet field for saying what to do instead. The thing being judged stays in the thread, which is scrollable, copyable and persisted; the panel is capped at half the viewport and could not hold a diff or a plan.Breaking, pre-1.0, no shims:
aparte-tool-decisionis deleted — the event,AparteToolDecisionDetail, its event-map entry and thedocumentlistener that answered it. It existed only because a segment renderer has no reference to the client. To answer programmatically, pass anapprovalResolveror register your own presenter; both see the whole request instead of an id on an event.AparteToolApprovalResolverandStreamApprovalResolvertake the CALL,(call, signal)rather than(toolCallId, signal). You cannot ask a person “run this?” without naming what — and an id alone forced a lookup table filled by one event and read by another, the shape that breaks in silence.- Both resolvers may return an
instruction, the words the model reads back on a refusal. AparteElicitationRequestgainskindandoptions, andschemais now optional — required on a'question', absent on an'approval'.
New:
buildApprovalPanelandBuiltApprovalPanel,AparteApprovalOptionandAparteApprovalAnswer, and four locale keys (approvalAsk,approvalWaiting,approvalInstructionPlaceholder,approvalOptionsLabel), translated in@aparte/locale-fr.<aparte-chat>now ships<aparte-elicitation>in its default composition. The built-in gate asks through the presenter, so a chat without one could not honourneedsApprovalat all. An affordance core honours end to end is on by default; leaving this out would have made the gate depend on a tag nobody was told to write. Author-provided compositions are untouched, as always.The options come with the request. Core supplies two — the tool’s name as the question, Approve and Reject — and anything richer is the host’s: a scope option (“and always for this tool”) exists only because an app wrote the label and can remember the grant. Core never invents one and never interprets one.
Also fixed, in passing: a panel’s own buttons inherited the composer row’s 44×44 action-control sizing and rendered as circles with their labels spilling out. Any panel containing a button hit this; the approval options were the first that do.
@aparte/locale-fr
Version-only bumps (no changes of their own): @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-transformers, @aparte/plugin-marked, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown.