0.18.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
8f2d28e: New: run the agent loop on your server.
createAparteAgentHandlerruns it next to your tools and streams it;createAgentStreamRunnerplugsAparteClientinto it — tool rows, Stop, the approval panel and your approval policy work as they do in the browser, and a user can leave in the middle of an approval — another conversation, a closed tab — and answer it when they come back.Until now a server with its own tools could only drive the chat display-only: the message, the segments, closing the turn and four lifecycle events, by hand. The handler pauses the loop on a call that needs a person, or on a tool the browser runs (
ask_user), and ends the stream with a signed continuation ticket; the page asks — the same panel, the same policy — and resumes with the answer, in the same bubble, under one pair of lifecycle events. Nothing is stored on the server: the paused call lives in the conversation you already save (AparteToolCallSegment.deferred) — saved the moment the run pauses, since the reply now dispatchesaparte-message-pausedand the conversation controller persists on it — and a conversation that loads with one resumes on its own — the conversation controller now dispatchesaparte-conversation-loaded, andclient.resumeDeferred()is there for a host that loads conversations another way. A message sent instead of an answer closes the paused call.The server decides which calls need a person; the page’s policy answers when it has an opinion, and otherwise the person is asked. The ticket makes the server run only the calls it emitted, for the reply it paused; it does not make a tool’s input trustworthy, and it is not single-use unless you pass
onTicket. A server tool checks who is calling with the request it gets ascontext.request(AparteToolContext.request, new). AtoolChoicenaming a tool, or a history ending on unanswered calls without a ticket, is refused with a 400, and a server tool’s own error text stays in the server’s log. An approvalpayloadis dropped unless the tool accepts one. Guide: Run the agent loop on your server.@aparte/core -
3776dfc: The model now remembers what its tools returned: each turn’s history carries the tool calls and their results, not only the text. If you serialize the history yourself — a backend of your own that does not go through an aparté provider or
toModelMessages— it now contains assistant messages withtoolCallsandrole: 'tool'messages, the shapes a tool round trip already used inside one turn: map them, or drop both. Through an aparté provider there is nothing to do. Requests grow on tool-heavy conversations;@aparte/plugin-compactionnow counts those calls when it measures a conversation, and summarises long ones as before.Until now
AparteClientsent user and assistant text alone, so aread_fileresult was on screen and gone from the model’s context one turn later — “what did the file say?” had no answer. A reply is now sent in the order it was shown: the text before some calls and those calls as an assistant message, each call’s result as atoolmessage, the text after them as the next assistant message. Every call is answered, because providers reject a call left without one: a refused call sends its reason, a failed one its error line, a stopped one “The tool call was stopped before it ran.” Send, retry and edit build the history through the same function, so they cannot disagree. The compactor’s selector used to count a tool call as free (it has nocontent); it now costs what its arguments and result send the model.@aparte/core,@aparte/plugin-compaction -
65845fe: New:
runStreamAgentcan pause on a tool call it cannot finish and resume it later. Passdeferraland a call that needs a person’s decision, or a tool markedexternal, ends the run onrun-suspendedinstead of waiting; a later run whose history ends on those calls, with their answers indeferral.outcomes, picks up where it stopped. If youswitchexhaustively overStreamRunEvent(an adapter of your own), add a case forrun-suspended; it only occurs when you passdeferral.Nothing changes for a caller that does not pass
deferral: the approval resolver still answers in place. The run that pauses reads the model’s reply to its end, so every call of that reply is pending at once and a client can answer them in one round trip; nothing runs after the pause point, and a call the gate denies is refused, never deferred. A resumed run answers its pending calls through the same code a streamed call takes, and emits notool-startfor them, since their rows exist already.unansweredTail(messages)returns the calls a history leaves unanswered. This is the loop half of running the agent on a server;@aparte/coreadds the server handler and the browser runner.@aparte/engine
Patch Changes
-
81661e9: Stopping a reply no longer prints an uncaught
AbortErrorin the console when the stream had already failed.An aborted
fetcherrors its response body, and the Stop handler then cancelled the reader.reader.cancel()returns a promise that rejects on an errored stream, and it was wrapped in atrythat only catches synchronous errors, so the rejection went unhandled. It is caught now.@aparte/core
Version-only bumps (no changes of their own): @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-scenario, @aparte/provider-transformers, @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-ask-user, @aparte/plugin-marked, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locale-fr, @aparte/docs-mcp.
0.19.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
16001e5: Your rebrand now wins over core’s palette whichever stylesheet loads first. A
[data-aparte-theme="dark"] { … }block now wins on a dark OS too (#95). Check one case: if your app setsdata-aparte-theme="dark"and rebrands only on:root, your:rootcolours now apply in that dark mode. Give dark a block of its own, as the theming guide shows under “Rebrand the dark palette”.Core’s palette is declared at zero weight (
:where()) wherever the host owns the choice: the light literals and both attribute blocks. Before,[data-aparte-theme="dark"](0,1,0) lost to core’sprefers-color-schemeblock (0,2,0) on a dark OS. A:rootrebrand loaded before core lost in light as well, which happens under Vite when the page’s stylesheet is a<link>in index.html.The OS block now applies only when no attribute is set, at (0,1,1) (
html:not([data-aparte-theme])). That weight is chosen. It sits above a bare:root, so a rebrand that only knows light stays light-only on a dark OS, and its light colours do not mix with core’s dark inks. It sits below:root:not([data-aparte-theme="light"]), the guide’s selector for a host that follows the OS with a dark palette of its own.A workaround with more weight, such as
:root[data-aparte-theme="dark"]or:root:root, still works. The extra weight is no longer needed.@aparte/core -
16001e5: New: an assistant reply records the provider, model and temperature it ran on (
AparteMessage.model), and a turn a server paused resumes on them after a reload instead of on the model selected by then.AparteClientwritesmodel: { providerId, modelId, temperature? }on the reply when a turn starts.resumeDeferreduses it when that provider is still registered, and otherwise falls back to the default, as before. A per-message model (modelId/providerIdonaparte-send) and a retry’s temperature were lost across a reload before.@aparte/core -
16001e5: New package:
@aparte/runner-ag-uirenders an agent served over AG-UI (Agno, LangGraph, Mastra, CrewAI, Pydantic AI) or a TanStack AI chat route in aparté. PasscreateAgUiRunner({ endpoint })asAparteClient’sstreamRunner.The agent runs its loop and its tools on its own server. aparté draws what it streams: the text, the reasoning, a row per tool call, and the approval panel when the agent pauses on a call that needs a person (an AG-UI interrupt, such as TanStack AI’s
needsApproval). The answer goes back in oneresumebatch. Tools the page registers go to the agent as frontend tools. When the agent calls one, the page runs it and the result goes back. The pause is saved with the conversation, as with the server loop. The wire (SSE or NDJSON) is parsed in the package, with@aparte/coreas its only peer: no AG-UI SDK is pulled in.A TanStack AI route must hand
chat()the run’sresume,threadId,runIdandparentRunId. Withmessagesalone it streams, but never resumes a pause. The package’s tests run TanStack AI’s own server for every round trip. Guides: Providers → AG-UI agents, and TanStack AI.@aparte/runner-ag-ui
Patch Changes
-
16001e5: Fixed: clicking a conversation in
<aparte-conversation-list>while the list re-rendered (a save had just moved a row) could do nothing. The list now waits until the click has landed before it redraws.A row replaced between the press and the release lost its click. Chromium sent the click to the list, where nothing handled it, and WebKit sent none. While a pointer is down on the list, a new
conversationsor any other change is held and drawn once, right after the click. Keeping the row node would not have been enough: a row moved to its new place leaves the document on the way, and the moving row is often the one pressed.@aparte/core
Version-only bumps (no changes of their own): @aparte/engine, @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-scenario, @aparte/provider-transformers, @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-ask-user, @aparte/plugin-compaction, @aparte/plugin-marked, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locale-fr, @aparte/docs-mcp.
0.19.1
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
3cbc85e: Fixed: with
createAgentStreamRunneror an AG-UI runner, the page no longer has to register a provider and select a model it never calls (#100). SetdefaultModeloncreateAparteAgentHandler: when the page names neither, the handler runs its only provider on that model.Before,
AparteClientdropped every send withSend dropped: no model is selectedunless the page registered a provider whose id matched a key of the server’sproviders. A retry or an edit did nothing, without a word. The server-loop guide’s browser snippet left this out, so following it as written sent nothing.Now a runner other than the page’s own loop (
runStreamAgent) needs no provider in the page. A provider and model the page names anyway (setModelConfig, a model selector) travel with the turn, registered or not, and the server uses them. With several providers, the handler answers 400 and asks the page to name one. A stub provider registered as a workaround keeps working and can be removed.@aparte/core -
1c6666a: New:
extraBodyoncreateOpenAICompatProvideradds vendor fields to every request body (#101). For example,extraBody: { thinking: { type: 'disabled' } }turns off a GLM model’s reasoning on Z.ai.It is the body’s counterpart of
extraHeaders. The fields the adapter writes stay its own (model,messages,temperature,max_tokens,stream,stream_options,seed,tools,tool_choice): anextraBodykey with one of those names is ignored.@aparte/provider-openai-compat
Version-only bumps (no changes of their own): @aparte/engine, @aparte/runner-ag-ui, @aparte/provider-ai-sdk, @aparte/provider-scenario, @aparte/provider-transformers, @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-ask-user, @aparte/plugin-compaction, @aparte/plugin-marked, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locale-fr, @aparte/docs-mcp.
0.19.2
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
c0d518b: A checked menu item (
role="menuitemradio"ormenuitemcheckboxwitharia-checked="true") now shows its check mark. Nothing to change in your code.The menu keeps a gutter on every item of a panel that holds a checkable one, as a hidden glyph, so the labels share one edge. That rule uses
:has(), which takes the weight of its argument, and it outweighed the rule that shows the glyph on the checked item: no menu showed which item was checked, the kit’s own example included. The showing rule now carries the same:has().@aparte/core -
cd4a48f: The open sidebar drawer shows its own background again instead of being greyed out by its scrim. Nothing to change in your code.
The open drawer is a stacking context, and its scrim is a child at
z-index: -1, which paints above the drawer’s own background. The panel came out dimmed: a white ground measured 166,166,166 under the default scrim. Only the children with a background of their own (the search field, the selected row) stayed bright. The drawer’s ground is now painted again by an::afterabove the scrim. It inherits the element’s background, so a ground set through--aparte-sidebar-bgor through a selector of your own is the one shown.@aparte/core
Version-only bumps (no changes of their own): @aparte/engine, @aparte/runner-ag-ui, @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-scenario, @aparte/provider-transformers, @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-ask-user, @aparte/plugin-compaction, @aparte/plugin-marked, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locale-fr, @aparte/docs-mcp.
0.19.3 Latest
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
d898de8: Per-chat theming now works on the React, Vue and Svelte chat root, and your own token overrides on
aparte-chatwin whatever order the stylesheets load in.The derived layer (the inks, the scales, the bubble colours) is worked out again on
[data-aparte-chat], the root the three wrappers render, so a chat given its own--aparte-primarythere gets its own user bubble and inks. Every anchor of that layer but:hostnow weighs nothing (:where()), like the palette:aparte-chat { --aparte-primary-ink: … }no longer loses when core’s sheet loads after yours, or on a chat that carriesdata-aparte-theme.If you theme a React, Vue or Svelte chat by setting a DERIVED token (a surface step, a radius or size step,
--aparte-link-color, a scale) on:rootor an element around the chat, list[data-aparte-chat]besideaparte-chatin that rule: the root now works those tokens out again, asaparte-chatalways did, so a value set above it no longer reaches inside. Masters such as--aparte-primaryor--aparte-radius-unitset on:rootare unaffected.@aparte/core -
866e468: The action bar no longer overlaps the branch picker’s next arrow, which is back to a full 24px target.
The bar pulls itself 6px toward the start of the line so its first icon lines up with the text above; after a visible branch picker that ran it 1.5px over the picker’s last arrow (axe target-size: 22.5px instead of 24px). The pull now applies only when the bar opens the row.
@aparte/core -
e57013a: Chrome 111, Safari 16.2, Firefox 121 and Samsung Internet 22 are supported again: the accent inks and the composer’s padding now fall back where relative colour syntax or the
lhunit is missing.Each
--aparte-*-inkis declared first as its accent mixed 40 / 60 with the text colour (on the default palette the worst case, a pressed soft wash on surface-3, measures 4.75:1 in light, and 6.58:1 on the page background), then, under@supports (color: oklch(from red l c h)), as the accent at the set lightness it was before. The editor padding reads the line height asembefore@supports (height: 1lh)takes over, to the same pixel. Browsers with both features draw exactly what they drew.@aparte/core -
3e3a7ef: The conversation list now has an accessible name, “Conversations” by default, taken from the new
conversationListlocale key.<aparte-conversation-list>is a navigation landmark, and a page with a second one (a site’s own nav) had two unnamed landmarks (axe landmark-unique). The name follows the locale; anaria-labeloraria-labelledbythe app sets, before or after the list connects, keeps priority. French: « Conversations ».@aparte/core,@aparte/locale-fr -
ae56657:
AparteCodeSegment.showLineNumbersandAparteErrorSegment.stackare deprecated: nothing reads them, and they will be removed in a later minor.No renderer or stylesheet draws line numbers, and the error renderer draws
contentanddetailsonly, so setting either field changed nothing. Put a stack trace indetailsto show it. Code that sets them keeps compiling until the removal, which a later changeset will announce.@aparte/core -
4a4cad8: Opening a tool row, a reasoning block or any
<details>in the transcript no longer leaves a blank under the reply.The bottom spacer, which lets the person’s message rise to the top, is now recomputed when a disclosure opens or closes. It used to keep its closed-row size, so opening a tool row lengthened the transcript by the row’s height (176px on the examples). Closes #105.
@aparte/core -
ddfaaae: A question or approval panel no longer steals the focus from a reader writing elsewhere, no longer lands on “Approve” while you type, and always announces itself.
- From a field outside the chat, an iframe or another chat, the focus stays where it is, and a polite status in the composer says the question.
- From the chat’s own editor (typing the next message while a reply streams), the focus goes to the panel itself, named by the question, not to its first option: Enter no longer approves a tool call by accident.
- From a button (the chat’s own or one of yours beside it) or from nowhere (a click in Safari, a screen reader’s activation), the focus goes to the first control, without scrolling the page when it came from nowhere.
- The focus is followed through open shadow roots, so a field inside a web component counts as a field.
- On close it goes back to whoever had it, or to the composer when that is gone or was
<body>. - An approval’s options are described by the question (
aria-describedby).@aparte/core
-
2f69e76: Eight types a public type already named are now exported: six element attribute interfaces,
AparteRequestMetaandAparteConversationsListener.AparteContextAttributes,AparteIconAttributes,AparteScrollRailAttributes,AparteSidebarAttributes,AparteSplitAttributesandAparteSuggestionsAttributeswere generated but missing from the package entry;AparteRequestMetais the type ofAparteChatRequest._meta;AparteConversationsListeneris whatAparteConversationManager.subscribetakes. A consumer can now name all of them. Types only: nothing changes at run time.@aparte/core -
3e474c5: A framework chat streaming with
injectTokenStreamnow shows Stop, stops on it, and saves the reply: the turn sends its own lifecycle events.injectTokenStream(React, Vue, Svelte, Angular) dispatchesaparte-message-startwhen it begins andaparte-message-done,-abortedor-errorwhen it ends, on the chat, stamped with its id, asAparteClientdoes. The composer’s Stop (aparte-abortfor this chat) stops the stream; another chat’s Stop does not. Code that sent these events by hand aroundinjectTokenStreamcan drop them; left in place, the composer and the conversation manager see each event twice, which they tolerate.@aparte/core -
04180a3: Four doc comments now say what the code does: the auto-compact rule, the titler’s default word count, the bubble’s
timestamp, and the composer’s internal bus.<aparte-context auto-compact>asks for a compaction on each turn that ends indanger, one request at a time, not once per crossing.setupTitler’sbudget, omitted, does not mean six words: the model keeps, among its six best-scored words, those it is at least half sure of, never fewer than three, and every word of a message of four words or fewer.- The bubble’s
timestampattribute no longer names a private method. - The composer’s
_on/_emitare marked@internaland leave the generated reference; from outside, listen foraparte-composer-change.@aparte/core,@aparte/plugin-titler
-
778202f: Neutral and secondary buttons draw their label in a darker ink on the ghost, outline and soft fills, so they meet AA contrast.
They were the two intents without an ink of their own, so their label read the fill colour: 4.19:1 for the neutral soft button, and 2.71:1 for secondary text on the page, against the 4.5:1 AA asks. They now derive
--aparte-neutral-inkand--aparte-secondary-inkthe way the five accents derive theirs; the neutral soft button measures 6.99:1 in light and 9.1:1 in dark. A button that sets only--aparte-btn-intentis unchanged.@aparte/core -
44aaee4: A button with no intent class now inks like a neutral one: an intent-less
--solidbutton and the selected segment of an intent-less or--surfacegroup meet AA contrast.Such a button is neutral, but it kept the dark constant ink instead of the ink derived from its fill, so its label read dark on the neutral fill at 3.38:1 in light. It now measures 5.50:1 in light (white on the fill) and 7.27:1 in dark. If you set
--aparte-btn-inkto change that label, set--aparte-on-neutralinstead: where relative colour syntax exists, the intent-less button no longer reads--aparte-btn-ink. Browsers without it keep the previous ink.@aparte/core -
0320655: The status line and a live reasoning block now speak the chat’s locale: no more English “Typing” or “Thinking” under a French locale.
<aparte-chat-status>withouttextnames itself, and tells a screen reader, from thetypingkey instead of an English literal, and follows a locale change. A reasoning block streamed from a model no longer carries a hard-codedlabel: 'Thinking', so the renderer names it from thethinkingkey and relabels it on a locale change, like a block restored from history. The English defaults lose their ellipsis, because they also name a block that is over:thinkingis “Thinking”,typingis “Typing”; in French,thinkingis « Réflexion ». Alabelyou set on a segment still wins. If you readsegment.labelon a streamed reasoning segment, it is nowundefined: fall back to the locale’sthinking, as the renderer does. A test matching “Thinking…” or “Typing…” (or « Réflexion en cours… ») reads the new words.@aparte/core,@aparte/locale-fr -
34160c3: A suggestion chip that sends now hands the focus to the composer, instead of dropping it to the page when its row hides.
With
empty-only, the row hides as soon as the message is on its way, and the chip a keyboard user had pressed went with it: the focus fell to<body>. When the chip held the focus, the composer now takes it after the send. A click that did not focus the chip changes nothing.@aparte/core -
5c777d5: Models now run on the graphics card by default when the browser has a WebGPU adapter, as documented; before,
'auto'fell back to WASM everywhere. To keep them on the processor, callsetComputeDevice('wasm').The worker resolves
'auto'itself, once:'webgpu'whennavigator.gpu.requestAdapter()answers,'wasm'otherwise. It is not passed on to Transformers.js, which would try WebNN first. A runner now receives the resolved device inctx.device. If a model then fails to load on WebGPU (a blocklisted driver, an operator the backend lacks), it is loaded again on WASM and the console says so; a forcedsetComputeDevice('webgpu')fails as asked.setComputeDevice('webgpu' | 'wasm')still forces one; there is no need to call it to turn the graphics card on.@aparte/provider-transformers -
6231952: The question on an answered receipt is drawn in the neutral ink, so it meets AA contrast on its success tint.
It used the muted text colour, which measured 4.29:1 on the success mark against the 4.5:1 AA asks. It now reads
--aparte-neutral-ink, which measures 7.17:1 in light and 8.56:1 in dark, and keeps the muted colour with a core that does not declare that token.@aparte/plugin-ask-user
Version-only bumps (no changes of their own): @aparte/engine, @aparte/runner-ag-ui, @aparte/provider-ai-sdk, @aparte/provider-openai-compat, @aparte/provider-scenario, @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-compaction, @aparte/plugin-marked, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/docs-mcp.