Skip to content

Changelog

Version 0.12.0

Every @aparte/* package ships at this version (they are released in lockstep).

Minor Changes

  • 2ac6080: Four critical defects from the 0.11.0 cold audit. Each one is a place where two things had to agree and nothing checked that they did.

    A refusal’s words now reach the model on the engine path too. runStreamAgent hardcoded “Tool execution was rejected by the user.” and never read decision.instruction — which its own resolver type declares and core’s inline loop uses. So on the recommended path (ratified decision #6), a user who refused a tool and typed “use the staging bucket instead” had those words dropped before the model saw them. Handing the model a turn after a refusal exists so that it reads the refusal; there was nothing to read.

    The parity suite could not see it: its resolver returned { approved } and never an instruction, so both loops agreed on a case neither ran. It supplies one now, and asserts the sentence survives rather than only that the two sides match — agreement alone passes if both drop it.

    Two chats on one page no longer fight over the presenter. <aparte-elicitation> entered <aparte-chat>’s default composition in 0.11.0, so two plain chats each registered on the same config — which held ONE slot. The second clobbered the first (chat A’s approval opened under chat B, and answering it there decided A’s tool call), and when B unmounted it cleared the slot, leaving A mounted with a working presenter that never re-registered: every later approval and every ask_user in A rejected no-presenter for the life of the page, silently, because that warning fires once per config.

    It is a stack now, each entry carrying the element that registered it, and a request naming a target is routed to the presenter in the same chat. AparteElicitationRequest.target was already documented as “used to resolve WHICH instance presents”; the single slot is what made that impossible.

    New: AparteConfig.removeElicitationPresenter(fn) withdraws one presenter by name. setElicitationPresenter(presenter, owner?) takes an optional owner element. setElicitationPresenter(null) still means “turn it off” and clears all.

    hidePanel() no longer orphans an open request. The no-token form called the silent teardown, which nulls onEvict without calling it — so the documented public call closed an approval panel without telling its owner. The promise stayed pending, the approval await has no timeout, and because requestUserInput chains each request on the previous one, no further question or approval on that config was ever presented again. The old JSDoc justified the silent branch as “what reset() needs”; reset() calls _evictPanel(), which notifies. Both forms notify now, except a token-matched close — that is the owner closing its own panel, and it already knows.

    Breaking, pre-1.0: [multiple]="false" used to turn multi-file selection ON. The bindings generator modelled every boolean attribute as a presence attribute, and core has two that are three-state — default on, off only via the literal "false": multiple on <aparte-composer-add-attachment> and submit-on-enter on <aparte-composer>. For those, false REMOVED the attribute, which the element reads as true. The only value that turned it off was a string no binding could produce and no template type could express.

    true now writes the empty presence value and false writes "false", and the attribute type widens to boolean | 'false' so OFF is expressible in React, Vue and Svelte as well. If you passed ''/undefined to force one of these off, it never worked; pass false (Angular) or 'false' (templates).

    Two attributes join the typed surface, having been read lazily via getAttribute and so invisible to the manifest: submit-on-enter on <aparte-composer> and action-id on <aparte-composer-action> — the latter being the only way to tell two custom composer buttons apart.

    And aparte-action-click now declares its detail type, so Angular’s (actionClick) emits AparteActionClickEventDetail instead of void with $event discarded. @aparte/core, @aparte/engine, @aparte/angular

  • 2ac6080: Seven behavioural defects from the same audit.

    A tool call keeps its state word through a config change. relabel rebuilt the row’s badge as the icon alone, so any setLocale, setIconProvider, registerTool or reset() deleted the localized word beside it — permanently, because a settled call gets no further update(). “Done” went bare and pending’s “Running” went empty; four of five statuses regressed. It now goes through stateBadge, whose own docblock claims “one function so render and update cannot disagree” and which relabel had never been folded into.

    Breaking, pre-1.0: the last bare CSS class is prefixed. The unknown-segment fallback emitted class="segment aparte-segment-unknown" — the rename had prefixed the second token and left the first, so 0.11.0’s claim that every class core emits is prefixed was false. .segment is Semantic UI’s base layout class, which is one of the two reasons that rename happened. If you styled .segment, it is .aparte-segment now.

    Three CSS rules no longer reach out of core. [data-status="resolved"] .aparte-tool-state and its two siblings were the only rules in the stylesheet whose leftmost compound is an unprefixed non-element selector. A host wrapping the chat in <div data-status="rejected"> re-tinted every completed tool call’s word red. Now scoped to .aparte-segment-tool-call.

    Stop in one chat no longer tears down another chat’s open question. The receive side already resolved its own chat host when no target attribute is set — which is all of raw core, since the documented markup sets none. The send side read the attribute only, so the abort carried targetId: undefined, and a missing id means “for everyone”.

    A number binding with no value no longer writes "NaN". Angular’s numberAttribute returns NaN for undefined, null, '' and any non-numeric expression, so [scrollThreshold]="cfg.threshold" on an unset field wrote scroll-threshold="NaN" — and parseInt('NaN' || '50', 10) is NaN because 'NaN' is truthy, so the transcript stopped following a streaming reply and the scroll-to-bottom button never hid. applyElementProps removes the attribute instead, restoring the documented default. 0 still writes, because 0% is a value.

    A CSS variable with no value is removed rather than stringified. props={{ '--aparte-select-bg': theme.selectBg }} on an optional field set the property to the token undefined — worse than leaving it alone, because a property that is set makes every var(--x, default) skip its fallback and become invalid at computed-value time, so the declaration is dropped and the control renders unstyled. An object became [object Object] the same way.

    @aparte/svelte: five events are bindable again. The on: surface derives from HTMLElementEventMap, which deliberately omits the events carrying no detail. Harmless while that only governed addEventListener — but declaring the tags removed SvelteHTMLElements’ catch-all index signature, so on:aparte-cancel (the stop button), on:aparte-composer-submit, on:aparte-reset-done, on:aparte-select-open and on:aparte-select-close stopped type-checking. All five are Angular @Output()s, so the wrappers were not at parity.

    Closed by deriving from core’s proxy list as well, which already enumerates every event an element dispatches on itself. APARTE_DEFAULT_UI_EVENTS becomes as const so the literals exist at the type level, and core exports the new type AparteUiEventName.

    @aparte/plugin-model-selector: the framework peer ranges are fixed. react: "^19.2.7" and svelte: "^4.2.0" were copied from the package’s own devDependency pins, excluding React 18 and Svelte 5 — both supported by the matching wrappers. An out-of-range peer that is present is an ERESOLVE conflict whether or not it is optional, so installing this plugin in a Svelte 5 app failed. Now ^18.0.0 || ^19.0.0 and ^4.0.0 || ^5.0.0, matching the wrappers. @aparte/core, @aparte/svelte

Patch Changes

  • 2ac6080: Seven behavioural defects from the same audit.

    A tool call keeps its state word through a config change. relabel rebuilt the row’s badge as the icon alone, so any setLocale, setIconProvider, registerTool or reset() deleted the localized word beside it — permanently, because a settled call gets no further update(). “Done” went bare and pending’s “Running” went empty; four of five statuses regressed. It now goes through stateBadge, whose own docblock claims “one function so render and update cannot disagree” and which relabel had never been folded into.

    Breaking, pre-1.0: the last bare CSS class is prefixed. The unknown-segment fallback emitted class="segment aparte-segment-unknown" — the rename had prefixed the second token and left the first, so 0.11.0’s claim that every class core emits is prefixed was false. .segment is Semantic UI’s base layout class, which is one of the two reasons that rename happened. If you styled .segment, it is .aparte-segment now.

    Three CSS rules no longer reach out of core. [data-status="resolved"] .aparte-tool-state and its two siblings were the only rules in the stylesheet whose leftmost compound is an unprefixed non-element selector. A host wrapping the chat in <div data-status="rejected"> re-tinted every completed tool call’s word red. Now scoped to .aparte-segment-tool-call.

    Stop in one chat no longer tears down another chat’s open question. The receive side already resolved its own chat host when no target attribute is set — which is all of raw core, since the documented markup sets none. The send side read the attribute only, so the abort carried targetId: undefined, and a missing id means “for everyone”.

    A number binding with no value no longer writes "NaN". Angular’s numberAttribute returns NaN for undefined, null, '' and any non-numeric expression, so [scrollThreshold]="cfg.threshold" on an unset field wrote scroll-threshold="NaN" — and parseInt('NaN' || '50', 10) is NaN because 'NaN' is truthy, so the transcript stopped following a streaming reply and the scroll-to-bottom button never hid. applyElementProps removes the attribute instead, restoring the documented default. 0 still writes, because 0% is a value.

    A CSS variable with no value is removed rather than stringified. props={{ '--aparte-select-bg': theme.selectBg }} on an optional field set the property to the token undefined — worse than leaving it alone, because a property that is set makes every var(--x, default) skip its fallback and become invalid at computed-value time, so the declaration is dropped and the control renders unstyled. An object became [object Object] the same way.

    @aparte/svelte: five events are bindable again. The on: surface derives from HTMLElementEventMap, which deliberately omits the events carrying no detail. Harmless while that only governed addEventListener — but declaring the tags removed SvelteHTMLElements’ catch-all index signature, so on:aparte-cancel (the stop button), on:aparte-composer-submit, on:aparte-reset-done, on:aparte-select-open and on:aparte-select-close stopped type-checking. All five are Angular @Output()s, so the wrappers were not at parity.

    Closed by deriving from core’s proxy list as well, which already enumerates every event an element dispatches on itself. APARTE_DEFAULT_UI_EVENTS becomes as const so the literals exist at the type level, and core exports the new type AparteUiEventName.

    @aparte/plugin-model-selector: the framework peer ranges are fixed. react: "^19.2.7" and svelte: "^4.2.0" were copied from the package’s own devDependency pins, excluding React 18 and Svelte 5 — both supported by the matching wrappers. An out-of-range peer that is present is an ERESOLVE conflict whether or not it is optional, so installing this plugin in a Svelte 5 app failed. Now ^18.0.0 || ^19.0.0 and ^4.0.0 || ^5.0.0, matching the wrappers. @aparte/plugin-model-selector

Version-only bumps (no changes of their own): @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-transformers, @aparte/plugin-ask-user, @aparte/plugin-marked, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/react, @aparte/vue, @aparte/locale-fr.