Changelog
Version 0.14.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
e58508a:
<aparte-chat-bubble>reads its message role fromdata-roleonly; arole="user"/role="assistant"attribute is no longer honoured. If you write bubbles by hand, writedata-role(the viewport and every wrapper already did).roleis ARIA’s attribute — the element sets it toarticleon itself — and reading a message role from it too meant filtering our own value back out at every turn. Pre-1.0 a rename lands as a rename, without an alias.@aparte/core -
ea6cfe0:
showPanel()now mounts the panel inside any<aparte-composer>descendant markeddata-aparte-panel-host. Without the marker nothing changes: the panel still goes right after the first<aparte-composer-input>.“After the input” is a position, not a choice — a layout with the input in a row and the panel meant for a block of its own had no way to say so, and a builder that lays the composer out for you needs to.
@aparte/core -
461a692: New
<aparte-context>: a gauge of the model’s context window. It reads each turn’s reported usage and the window the current model declares (or awindowattribute), setsdata-leveltook/warn/dangerat thewarn/dangerfractions (75 % / 90 %), firesaparte-context-thresholdwhen the level changes, and withauto-compactdispatchesaparte-compacton reaching danger. New in@aparte/engine:createCompactionSelector({ contextWindow, systemPrompt }), the budget-awarecompactionSelectorforAparteClient— the newest turns that fit stay verbatim, the rest is summarised. New locale keycontextLabel, translated in@aparte/locale-fr.The first product built on the library showed a context badge that turned red at 90 % — and then nothing happened, because
compact()existed,compactionSelectorexisted, the engine’s compactor existed, and no piece joined them. This is the join: the gauge watches, the selector decides, and the two read the same window.@aparte/core,@aparte/engine -
04289bb:
@aparte/corenow depends on@aparte/engine— first-party, nothing from outside@aparteis installed — sonpm i @aparte/coreinstalls both; nothing changes in how you call either package.AparteStreamRunEvent,AparteStreamRunEmitter,AparteStreamRunOptionsandAparteStreamRunnerare engine’sStreamRunEvent,StreamRunEmitter,StreamRunOptionsand runner shape under core’s names.@aparte/engineno longer lists core as a peer dependency, andcreateCompactionSelectoris typed structurally (CompactableMessage), so it takes core’s messages without importing core.Decision D1 of the 2026-08-28 audit. The run-event contract was hand-mirrored across a “zero-import” boundary and policed by a compile-time guard that had itself been written around the one field that broke the seam; the same tool turn corrupted the history in two different shapes, one per loop, invisible to the parity suite precisely because they differ. The direction is settled: the loop is engine’s, core drives it. This is the first half — the types; the inline loop’s deletion is the second.
@aparte/core,@aparte/engine -
4bde588: A non-ok response from
AparteDirectTransportorAparteBackendTransportnow throws anAparteErrorwith the vendor’s message,httpStatusand acoderead off the status; until now every one of them reached the error card andaparte-message-errorasUNKNOWN_ERROR, whatever the vendor had said. A listener that matchedcode === 'UNKNOWN_ERROR'to catch transport failures should match the new codes (or the class) instead. The table:429→USAGE_RATE_LIMIT,401/403→CONFIG_INVALID_KEY(new code),503→PROVIDER_UNAVAILABLE, other5xx→PROVIDER_ERROR,400→USAGE_BAD_REQUEST,408→NET_TIMEOUT.AparteError.from()applies the same table to any error that carries astatus, readsfetch’s network failure (aTypeErrornaming the fetch) asNET_ERROR—NET_OFFLINEwhennavigator.onLineis false — and aTimeoutErrorasNET_TIMEOUT; a code the caller names is kept.AparteError.codeForStatus(status)is exported for a provider that wants the same mapping. A404stays unclassified on purpose: it is a wrong model or a wrong URL, and the message says which.@aparte/core -
129e094: Two chrome strings now follow the locale: the scroll-to-bottom button’s accessible name (
scrollToBottom) and the title of the messagecompact()injects (compactionSummaryTitle, no emoji any more).@aparte/locale-frships both.Both were hardcoded English in an otherwise localised transcript — a French chat compacted into a ”📝 Conversation summary” header, and its one floating button was announced in English. The keys are optional on
AparteLocale, so an existing locale package keeps compiling and falls back to English per key until it adds them. The engine compactor’s ownsummaryLabelis unchanged: it is a per-call knob on the prompt side, this is the UI title.@aparte/core,@aparte/locale-fr -
f9cac24: Older messages no longer reserve a row for their action bar; it floats over the message’s header row on hover or focus, as a small bordered toolbar. The transcript tightens by 34px per turn. Three bars stay in the flow as before — the last assistant message’s (always visible under the reply), a message’s whose branch picker is showing (the bubble now stamps
data-brancheson.aparte-messagewhile it does), and every bar on a device that cannot hover, where the bar is now also visible instead of sitting at opacity 0 with nothing able to reveal it. A stylesheet that positioned.aparte-footeror styled.aparte-action-barfor older messages should be checked against the new@media (hover: hover)rules inbubble.css.Measured on the vanilla example: 103px between the text of one turn and the next, 34 of them this footer under every message. The bar floats inside the message box rather than below it because a bubble is a paint-containment boundary (
content-visibility), which clips anything outside.@aparte/core -
0850dee:
_meta.pipeline,_meta.artifactRawand_meta.artifactXmlare removed fromAparteChatRequest, and with them thepipeline-waitingsegment and engine’s artifact-XML state machine._meta.artifactHintand_meta.prefixSegmentsstay and are documented; an<artifact>tag in the reply’s text is parsed exactly as before, and the built-increate_artifacttool is unchanged. Gone in full:ApartePipelinePhase, thepipeline-waitingsegment type with its renderer, its stylesheet andApartePipelineWaitingSegment, and — in@aparte/engine—ArtifactXmlStateMachine, its types and thephase-advance/artifact-open/artifact-chunk/artifact-closerun events.Decision D2 of the 2026-08-28 audit. The multi-phase pipeline and the raw-artifact turn were one product’s orchestration wearing a library type — nothing in this repository emitted either, and a contract nothing exercises is maintained for nobody. The XML mode was a second path to what the stream parser already does natively with
<artifact>tags, kept alive by a state machine that had to be mirrored between two loops. One path is left, the parser’s, and the loop no longer branches on the request’s metadata at all.@aparte/core,@aparte/engine -
cd5075e:
AparteClientruns@aparte/engine’srunStreamAgentby default; core’s inline copy of the agent loop is deleted. One behaviour changes: a tool call stopped while it waited for approval is now markedaborted, neverrejected, and a host that stops the turn from the approval panel itself no longer leaves the call stuck atawaiting-approval. Nothing else changes in how you call either package.streamRunnerstays, to wrap or replace that loop ((opts) => runStreamAgent({ ...opts, onHistoryAppend })for a host that owns its transcript).deriveArtifactKindis the engine’s, re-exported by core under the same name.Decision D1 of the 2026-08-28 audit, second half. Two copies of one loop were “kept in sync” by hand and by a parity suite; the same tool turn corrupted the history in two different shapes, one per copy, invisible to that suite precisely because they differed. The suite’s 26 scenarios were snapshotted while both loops ran and were equal — the inline loop’s behaviour, pinned — and now live in core, where they also hold the client’s wiring to a direct engine run. That is what found the client writing
status: 'streaming'once too often, and what dropped the change from 2 470 lines of client to 1 750.@aparte/core,@aparte/engine -
95c390d: Two new tokens let a host match the chat’s scrollbar to its own page:
--aparte-scrollbar-thumb(derived from--aparte-neutral) and--aparte-scrollbar-track(transparent), beside the existing--aparte-scrollbar-width. A host page with a styled scrollbar of its own sets them onaparte-chatso the chat’s does not read as a second, foreign scrollbar — the docs site does this now. Defaults are unchanged; a stylesheet that overrodescrollbar-coloron.aparte-viewport-containerkeeps working.@aparte/core -
9cf00bb:
AparteSendEventDetaildeclaresmodelIdandproviderId: anaparte-sendcarrying them sends that one message to that model, overriding the config’s default for the turn — a per-message model picker.AparteClienthas honoured both fields for as long as it has readevent.detail, while nothing declared them and the composer never sent them, so the capability existed only for whoever read the client’s source. Declaring it is what makes it real; the generated events reference picks it up.@aparte/core -
1412c54:
setSkeletonProvider,getSkeleton,AparteSkeletonProvider,AparteSkeletonTypeandAPARTE_DEFAULT_SKELETON_FALLBACKSare removed from@aparte/core, andprovideAparte({ plugins: { skeleton } })from@aparte/angular. The.aparte-skeletonCSS recipe stays. If you registered a skeleton provider, delete the call: nothing read it.Nothing in core ever called
getSkeleton()— no component has a loading state that is not the message itself, so the seam was a contract with no consumer on either side, and the six fallback strings it shipped (and their four CSS classes) were dead weight in every bundle. A consumer who wants a placeholder uses the recipe, which is the part that was real.@aparte/core,@aparte/angular -
1412c54:
AparteStorageAdapterloses its optional memory-fact, settings and artifact-gallery methods, and theAparteMemoryFact/AparteArtifactRowtypes are gone.loadAttachmentsandAparteAttachmentRowstay. An adapter that implemented the removed methods still compiles — they were optional — but the types it named have to come from your own code now.The shape of a “memory fact” (
identity | fact | preference | tech | project | style, asourceofautooronboarding) and of a settings entry is one product’s schema, not a chat library’s; a public contract that carries it binds every other adapter to that product’s choices. Core never read any of those methods. The contract is now exactly what the chat needs persisted — conversations, their tree, their attachments — and an app extends the interface in its own code for the rest.@aparte/core -
e413352: New
<aparte-suggestions>: a row of prompt starters. Give itsuggestions='["…", {"label": "…", "prompt": "…"}]'(or set thesuggestionsproperty), and a click fills the composer and submits it;mode="fill"only fills and focuses,empty-onlyhides the row after the first send,targetnames the chat when the element sits outside its composer. It fires a cancelableaparte-suggestionfirst. New locale keysuggestionsLabel(the group’s accessible name), translated in@aparte/locale-fr.Every chat product opens on three or four of these, and the example app hand-rolled them — four buttons, a click handler, a CSS recipe of its own. The click goes through the composer’s
submit()on purpose: that is where every gate lives (disabled, streaming,requireModelSelection), and a chip that bypassed them sent a request with an empty model id while the composer was visibly greyed out. The chips wear theaparte-btnrecipe, so a theme reaches them with no knob of their own.@aparte/core -
8da979c: A new CSS class,
aparte-mark, gives a chosen row one look everywhere: an intent tint on its ground and a bar on its start edge.aparte-mark--success,--danger,--neutraland--quietpick the intent (primary by default), and two tokens move every mark at once:--aparte-mark-tint(18%) and--aparte-mark-bar(2px). The select’s chosen option, a checked field choice and the active conversation wear it; any row, option or button can.The recipe lives in
display/mark.css. The bar is drawn in the intent’s ink so it reads at 3:1 and above (the raw success fill was 2.27:1 on the light surface);--quietis the outcome that did not happen: no tint, no bar, muted. The bar is a::beforepseudo-element on the logical start edge, so a right-to-left row —diron the document, on the row, orauto— gets it on the right edge. The select’s chosen option keeps its look and now reads those tokens; a checked field choice (the elicitation panel’s options) gains the tint and the bar beside its primary border, and keeps them under the pointer; the active conversation gains the bar (its ground stays the list’s own).Tool-call rows: rejected and aborted no longer share the error ink — both keep the muted voice, and the glyph tells them apart (a cross for rejected, a stop square for aborted). Red stays for what went wrong.
@aparte/core -
1f654b0: The composer stays editable while a reply streams: the next message can be typed and files attached while the current reply arrives, as in every chat. Only the send is gated meanwhile — the button is Stop, and Enter neither sends nor stops (the draft stays and Enter sends it once the turn is over). Until now the editor and the attach button went inert for the whole turn.
disabledstill makes the editor non-editable (therequire-modelgate never did — it gates the send, and typing under it is what the browser suite checks), and a non-editable editor now leaves the tab order (tabindex="-1") and drops focus, so clicking it no longer lights the shell’s focus border on a field that cannot be typed in. A custom<aparte-composer-action>keeps its own rule (disabled while streaming), since its act is the host’s.@aparte/core -
e58508a: The deprecated
max-messagesattribute andmaxMessagesoption of<aparte-chat-viewport>are removed — usemax-rendered-bubbles/maxRenderedBubbles, which is what the alias had been forwarding to.Pre-1.0 a rename lands as a rename; the alias and its one-time warning were the one deprecation shim in the package. (
AparteConversationManager’s ownretention.maxMessagesis unrelated and unchanged.)@aparte/core -
1912df6: New package:
@aparte/provider-scenario, a scripted model.createScenarioProvider({ turns })answers the model’s calls in order;createScenarioProvider({ scenarios })picks a named turn bywhen(the last user message) orafter(a tool’s result). A turn is text streamed at a typing pace, thinking, a tool call the real loop runs, an error, a pause, a usage override.showcaseis a ready-made set covering the whole surface of a chat. No key, no network, no dependency of its own.Three things in this repository had written it by hand: the browser suite’s wire mock, the UI audit’s screenshot harness, the docs’ live frames. It is also the piece nobody ships for consumers — a deterministic model for their own tests, and a demo that streams without a backend. The repository’s browser suite keeps its network mock on purpose: it tests the wire path this provider bypasses.
@aparte/provider-scenario -
b4c7365:
createAskUserToolandsetupAskUseracceptname,descriptionandsystemPrompt;setupAskUseralso takesreceipt: falseto keep the transcript silent. Nothing changes when you pass none of them.The tool’s name was
ask_userthree times over (the tool, the receipt renderer, the Node entry) and its description and system prompt were fixed English — so a backend that already exposed anask_user, or a product that wanted the model to read another policy in another language, had to fork the tool for two strings. The receipt renderer now registers under whatever name is chosen, and declining the receipt no longer means registering an empty renderer aftersetupAskUserand hoping the order holds.@aparte/plugin-ask-user -
91607bd: The question receipt now shows a success tint and a bar on its start edge once answered (
aparte-mark aparte-mark--success), and a muted, unmarked look when declined (aparte-mark--quiet) — core’saparte-markrecipe, so it matches the select’s chosen option and a checked field choice. And aquestion-receiptsegment an app emits itself can now saydeclined: true— it renders the outcome alone, the way the tool’s own receipt already did.@aparte/plugin-ask-user -
a57ad06:
themeaccepts a{ light, dark }pair on both entries; the code block then follows core’s[data-aparte-theme="dark"]switch. A single theme works exactly as before.One theme paints one scheme, so
github-dark— the default — stayed a dark slab inside a light chat, and no option could say otherwise: the plugin’s most visible gap. With a pair, shiki renders both colours per token as CSS variables (--shiki-light/--shiki-dark, its own dual-theme output,defaultColor: false) and the plugin adds one small stylesheet that picks the one the theme attribute asks for. Core learns nothing about shiki: the attribute is core’s public theme contract, the variables are shiki’s.@aparte/plugin-shiki
Patch Changes
-
9c4ef91: Attachments under a sent message render as real tiles — the same thumbnail tiles the composer previews — instead of a bare “PDF” beside an unframed image. The attachment strip and tile rules moved from
composer.cssto the display layer (thumbnail.css), where a recipe shared by two components belongs.If you restyled the strip through
.aparte-attachmentsor.aparte-thumb…selectors nothing changes: the class names are the same, only the sheet that declares them.@aparte/core -
d22a75d:
AparteClient.abort()now stops an in-flightcompact()without disturbing a turn, and a turn without disturbing a compaction.Compaction used to borrow the turn’s abort controller slot, so a summarisation started during a turn left that turn unabortable — Stop reached only the summary while the reply kept streaming and kept being billed. Each has its own controller;
abort()fires both.@aparte/core -
d45da0c: Changing
placeholderon<aparte-composer>now updates an<aparte-composer-input>already on the page.syncPlaceholder()on the input is the method the composer calls; an input with aplaceholderof its own is unaffected.The input read the composer’s placeholder as a fallback when it rendered and never again, and the composer’s attribute callback for it was an empty branch — so a placeholder bound to a translated string went stale on the first language switch after mount.
@aparte/core -
64f679a: The bubble’s copy button copies the reply without its reasoning block.
It joined every segment’s content, so a reply that opened with a
thinkingsegment pasted the model’s deliberation above the answer. The client already keeps that block out of the history it sends back, for the same reason; the two rules for “what the reply is” now agree.@aparte/core -
9c4ef91: An assistant turn that ends with nothing to show — stopped before its first token, or made only of a tool that renders nothing — no longer leaves a name and a timestamp floating in the transcript. The bubble sets
data-emptyon.aparte-messageand the stylesheet hides the row; restyle.aparte-message[data-empty]if you want a “stopped” marker instead.Streaming bubbles are never empty (the waiting dots are their content), and attachments count as content. The element stays in the DOM, so streaming and the action bar still address it by id.
@aparte/core -
213add8: A code block no longer closes on a streamed chunk that merely ends in three backticks — a fence has to start a line.
const s = "```"split by the tokenizer right after the quotes used to close the block mid-code, and the rest of the file streamed as prose. A reply that genuinely ends on ``` with no newline is still handled: the fence is stripped once, at the end of the stream, where it cannot mis-close anything.@aparte/core -
e083712: The 25 built-in glyphs and the 41 icons behind
@aparte/core/iconsare redrawn. Every name, export and size is the same — an icon provider you registered, and any--aparte-icon-sizea container declares, are unaffected.They are core’s own drawings, on one grid (24 units, a 2-unit round-capped stroke,
currentColor) so the two sets keep a single optical weight side by side, and the package carries no notice and credits no icon set. The generated icons reference describes the grid and the naming rather than pointing at any particular set.@aparte/core -
1589baa: Markdown tables in a reply are styled: borders, cell padding, a header row on the surface tone, and a wide table scrolls inside the bubble instead of overflowing it.
The sanitizer had allowlisted
table/th/tdfrom the start and no stylesheet ever drew them, so a GFM table rendered as words with no borders and columns that touched.prose.cssstyles it like the rest of the prose, from existing tokens only.@aparte/core -
e3d0006: Scrolling up while a reply streams now sticks: the transcript stops pulling the reader back to the bottom.
Auto-follow was disarmed by the gesture, but a scroll-to-bottom frame queued just before it still ran — and the bottom it reached re-armed auto-follow, so every attempt to read something above the stream lasted one frame. Queued frames now re-check the intent before scrolling. Reaching the bottom again, or pressing the scroll-to-bottom button, re-arms it as before.
@aparte/core -
2c67b6b:
<aparte-select>’s dropdown panel reads--aparte-select-dropdown-bgin the dark theme too. Its dark rule used to repaint the panel from--aparte-select-bg— the trigger’s background — so a transparent trigger (a pill on a coloured page) made the open list see-through in the dark, with the page’s text showing through the options. The dark override is gone altogether: every colour of the select reads a token the derived layer already resolves per theme. And the trigger’s label follows a list refreshed in place (a consumer writing into.aparte-select-options, as the model selector does): it kept showing a label the list no longer offered.@aparte/core -
16464cd:
<aparte-select>keeps one width — its widest option’s — whatever is selected, like a native<select>; it used to resize to the selected label on every change. The trigger’s label is now a grid of two layers (.aparte-select-label-textand a hidden.aparte-select-label-sizerstack of every option’s label); a stylesheet that targeted.aparte-select-label’s text directly should target.aparte-select-label-text. A host that constrains the control narrower than its widest option still gets an ellipsis.@aparte/core -
8d07938: On WebKit the transcript no longer settles a few pixels short of the bottom — with a scroll-to-bottom button showing — when a streamed reply ends or a branch is swapped at the bottom.
The action bar appearing at the end of a stream and the bottom spacer giving those pixels back happen in one frame, and through that churn WebKit moves
scrollTopbackwards; a branch swap on React flickers the height by ~200px and moves it by as much. Since queued scroll frames re-read the reader’s intent, those browser-made decreases read as “the reader went up” and disarmed the follow mid-landing. A decrease now counts as the reader’s unless three things hold: it is no larger than the scroll height moved since the last scroll event (churn movesscrollTopby at most the height it changed; a reader, a find-in-page jump or a host’sscrollTomove it with the height standing still), it comes within a second of a scroll the viewport asked for, and no scroll gesture touched the transcript in that second — a wheel notch, a touch that moves, a navigation key, or a press in the scrollbar’s gutter. A click or a tap on a control inside the transcript (a branch arrow, copy) is not a scroll gesture.@aparte/core -
f9a6fbd: While a reply streams, the transcript is read-only except for Stop and copy: the branch pickers and the retry/edit actions of every message are disabled, and
navigateBranch()is a no-op. Until now only the streaming message’s own footer was hidden: swapping a branch on an older message re-rendered the active path under the reply being written, and a retry cut that reply off to start another. The viewport carriesdata-busywhile it streams and pushes the state to its bubbles (setTranscriptBusy()); a bubble mounted under a framework’s DOM while the flag is up reads it on connect.A stopped reply now reaches a terminal status on every path, so the flag comes down. Two paths did not settle the message: a stream stopped through the host (
stopTokenStream()/ a wrapper’s stop left the viewport holding the message as streaming — it “kept what was streamed” but never finished it), and a Stop pressed before the first token arrived (while auth or an attachment was still being read). Either one left the transcript read-only for the life of the page.clearAll()clears the flag too.@aparte/core -
f9b1008: Four visual fixes: popovers and the select dropdown cast a visible shadow, the recommended elicitation option shows one focus ring instead of two, the bubble’s action-bar buttons reach the touch-target size on a coarse pointer, and
@aparte/plugin-ask-user’s receipt shows the answer in the strong text colour instead of green. The shadows are--aparte-popover-shadowand--aparte-select-shadow— set them yourself if you had: on cream the old one was imperceptible. The recommended option no longer shows its tinted border under the focus ring — one ring at a time. On a coarse pointer the action-bar buttons grow like the other controls already did. And the receipt’s green was the one hue outside the palette on the whole transcript.@aparte/core,@aparte/plugin-ask-user -
9592bed: On a page with several chats, an
<aparte-composer>that belongs to none — notarget, no chat host with an id above it — logs one warning saying how to attach it. Nothing else changes.Such a composer answers to every chat’s lifecycle events, so one chat’s Stop evicted another’s open question, and the symptom sat nowhere near its cause. A signal at the console, not a guard.
@aparte/core -
d299096: A turn that calls
create_artifactand another tool no longer sends the second tool’s result without the call that declares it — a history an Anthropic-shaped API rejected outright. The turn’stool_callenvelope is now created once, held by reference, and declares every call whosetool_resultfollows it;onHistoryAppendreports the envelope once, when the first call completes, and the calls that complete later in the same turn are already in that same object’stoolCalls.The loop used to guess whether the turn’s envelope was already in the history by scanning it for any of the turn’s call ids. The built-in
create_artifactfast path pushed a fresh envelope of its own, the scan found the artifact’s id in it, concluded “already pushed”, and the next tool’s result went out orphaned. A reference cannot be guessed wrong.@aparte/engine -
461a692: New
<aparte-context>: a gauge of the model’s context window. It reads each turn’s reported usage and the window the current model declares (or awindowattribute), setsdata-leveltook/warn/dangerat thewarn/dangerfractions (75 % / 90 %), firesaparte-context-thresholdwhen the level changes, and withauto-compactdispatchesaparte-compacton reaching danger. New in@aparte/engine:createCompactionSelector({ contextWindow, systemPrompt }), the budget-awarecompactionSelectorforAparteClient— the newest turns that fit stay verbatim, the rest is summarised. New locale keycontextLabel, translated in@aparte/locale-fr.The first product built on the library showed a context badge that turned red at 90 % — and then nothing happened, because
compact()existed,compactionSelectorexisted, the engine’s compactor existed, and no piece joined them. This is the join: the gauge watches, the selector decides, and the two read the same window.@aparte/locale-fr -
e413352: New
<aparte-suggestions>: a row of prompt starters. Give itsuggestions='["…", {"label": "…", "prompt": "…"}]'(or set thesuggestionsproperty), and a click fills the composer and submits it;mode="fill"only fills and focuses,empty-onlyhides the row after the first send,targetnames the chat when the element sits outside its composer. It fires a cancelableaparte-suggestionfirst. New locale keysuggestionsLabel(the group’s accessible name), translated in@aparte/locale-fr.Every chat product opens on three or four of these, and the example app hand-rolled them — four buttons, a click handler, a CSS recipe of its own. The click goes through the composer’s
submit()on purpose: that is where every gate lives (disabled, streaming,requireModelSelection), and a chip that bypassed them sent a request with an empty model id while the composer was visibly greyed out. The chips wear theaparte-btnrecipe, so a theme reaches them with no knob of their own.@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-model-selector, @aparte/plugin-streaming-markdown, @aparte/react, @aparte/svelte, @aparte/vue.