Skip to content

0.17.3

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

Patch Changes

  • @aparte/engine@0.17.3 @aparte/core

  • 5ec1e8a: Fixed: in controlled mode (messages with onMessagesChange), a finished reply could roll back to an earlier state — its text cut mid-word, its tool rows gone. It happened when the parent re-rendered late, which React does in Safari.

    The chat emits its list as it streams, the parent stores it, and the prop comes back. When the parent’s render lands after the chat’s next write, the prop that arrives is an EARLIER emit, and the wrapper took it for the parent’s word: it rolled the list back, the chat built its next writes on the stale copy, and the reply stayed broken. Measured in the examples’ browser suite at about one run in five on React + WebKit. The wrappers now recognise their own emits when they come back — current or stale — and ignore them; a list the parent builds itself is still applied. Angular is unaffected: its list is one model() signal both sides write in step. @aparte/react, @aparte/svelte, @aparte/vue

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/locale-fr, @aparte/docs-mcp.

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. createAparteAgentHandler runs it next to your tools and streams it; createAgentStreamRunner plugs AparteClient into 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 dispatches aparte-message-paused and the conversation controller persists on it — and a conversation that loads with one resumes on its own — the conversation controller now dispatches aparte-conversation-loaded, and client.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 as context.request (AparteToolContext.request, new). A toolChoice naming 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 approval payload is 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 with toolCalls and role: '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-compaction now counts those calls when it measures a conversation, and summarises long ones as before.

    Until now AparteClient sent user and assistant text alone, so a read_file result 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 a tool message, 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 no content); it now costs what its arguments and result send the model. @aparte/core, @aparte/plugin-compaction

  • 65845fe: New: runStreamAgent can pause on a tool call it cannot finish and resume it later. Pass deferral and a call that needs a person’s decision, or a tool marked external, ends the run on run-suspended instead of waiting; a later run whose history ends on those calls, with their answers in deferral.outcomes, picks up where it stopped. If you switch exhaustively over StreamRunEvent (an adapter of your own), add a case for run-suspended; it only occurs when you pass deferral.

    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 no tool-start for 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/core adds the server handler and the browser runner. @aparte/engine

Patch Changes

  • 81661e9: Stopping a reply no longer prints an uncaught AbortError in the console when the stream had already failed.

    An aborted fetch errors 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 a try that 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 sets data-aparte-theme="dark" and rebrands only on :root, your :root colours 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’s prefers-color-scheme block (0,2,0) on a dark OS. A :root rebrand 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.

    AparteClient writes model: { providerId, modelId, temperature? } on the reply when a turn starts. resumeDeferred uses it when that provider is still registered, and otherwise falls back to the default, as before. A per-message model (modelId/providerId on aparte-send) and a retry’s temperature were lost across a reload before. @aparte/core

  • 16001e5: New package: @aparte/runner-ag-ui renders an agent served over AG-UI (Agno, LangGraph, Mastra, CrewAI, Pydantic AI) or a TanStack AI chat route in aparté. Pass createAgUiRunner({ endpoint }) as AparteClient’s streamRunner.

    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 one resume batch. 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/core as its only peer: no AG-UI SDK is pulled in.

    A TanStack AI route must hand chat() the run’s resume, threadId, runId and parentRunId. With messages alone 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 conversations or 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 createAgentStreamRunner or an AG-UI runner, the page no longer has to register a provider and select a model it never calls (#100). Set defaultModel on createAparteAgentHandler: when the page names neither, the handler runs its only provider on that model.

    Before, AparteClient dropped every send with Send dropped: no model is selected unless the page registered a provider whose id matched a key of the server’s providers. 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: extraBody on createOpenAICompatProvider adds 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): an extraBody key 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" or menuitemcheckbox with aria-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 ::after above the scrim. It inherits the element’s background, so a ground set through --aparte-sidebar-bg or 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-chat win 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-primary there gets its own user bubble and inks. Every anchor of that layer but :host now 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 carries data-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 :root or an element around the chat, list [data-aparte-chat] beside aparte-chat in that rule: the root now works those tokens out again, as aparte-chat always did, so a value set above it no longer reaches inside. Masters such as --aparte-primary or --aparte-radius-unit set on :root are 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 lh unit is missing.

    Each --aparte-*-ink is 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 as em before @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 conversationList locale 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; an aria-label or aria-labelledby the app sets, before or after the list connects, keeps priority. French: « Conversations ». @aparte/core, @aparte/locale-fr

  • ae56657: AparteCodeSegment.showLineNumbers and AparteErrorSegment.stack are 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 content and details only, so setting either field changed nothing. Put a stack trace in details to 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, AparteRequestMeta and AparteConversationsListener.

    AparteContextAttributes, AparteIconAttributes, AparteScrollRailAttributes, AparteSidebarAttributes, AparteSplitAttributes and AparteSuggestionsAttributes were generated but missing from the package entry; AparteRequestMeta is the type of AparteChatRequest._meta; AparteConversationsListener is what AparteConversationManager.subscribe takes. A consumer can now name all of them. Types only: nothing changes at run time. @aparte/core

  • 3e474c5: A framework chat streaming with injectTokenStream now shows Stop, stops on it, and saves the reply: the turn sends its own lifecycle events.

    injectTokenStream (React, Vue, Svelte, Angular) dispatches aparte-message-start when it begins and aparte-message-done, -aborted or -error when it ends, on the chat, stamped with its id, as AparteClient does. The composer’s Stop (aparte-abort for this chat) stops the stream; another chat’s Stop does not. Code that sent these events by hand around injectTokenStream can 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 in danger, one request at a time, not once per crossing.
    • setupTitler’s budget, 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 timestamp attribute no longer names a private method.
    • The composer’s _on / _emit are marked @internal and leave the generated reference; from outside, listen for aparte-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-ink and --aparte-secondary-ink the 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-intent is unchanged. @aparte/core

  • 44aaee4: A button with no intent class now inks like a neutral one: an intent-less --solid button and the selected segment of an intent-less or --surface group 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-ink to change that label, set --aparte-on-neutral instead: 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> without text names itself, and tells a screen reader, from the typing key instead of an English literal, and follows a locale change. A reasoning block streamed from a model no longer carries a hard-coded label: 'Thinking', so the renderer names it from the thinking key 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: thinking is “Thinking”, typing is “Typing”; in French, thinking is « Réflexion ». A label you set on a segment still wins. If you read segment.label on a streamed reasoning segment, it is now undefined: fall back to the locale’s thinking, 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, call setComputeDevice('wasm').

    The worker resolves 'auto' itself, once: 'webgpu' when navigator.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 in ctx.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 forced setComputeDevice('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.