0.27.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
5e31fa9: The keyboard focus ring is now 2px wide with a 2px gap, instead of 1px flush against the control. For the old ring, set
:root { --aparte-focus-outline-width: 1px; --aparte-focus-outline-offset: 0px; }.2px is the thickness WCAG 2.2 asks of a focus indicator (2.4.13). The ring keeps its neutral colour, which is what kept a page full of focusable controls from looking framed. Measured on every component and template page of the docs, focused as a keyboard would: no ring there was newly cut by the gap. One control that appears only once a file is attached was — a pending file’s ✕, inside a tile that clips — and it now draws its ring inside, like the picture beside it.
The approval panel leaves the wider ring room too:
--aparte-approval-gapnow defaults to the ring’s reach plus one spacing step (6px), and--aparte-elic-paddingnever pads the sides below that reach, so a focused option’s ring neither touches the next option nor is cut by the panel at a denser--aparte-space-unit.@aparte/core -
5e31fa9: A
streamRunnernow receives the files attached to the message it answers, asfilesin its run options — so a runner whose backend takes uploads can send them as files, PDFs and spreadsheets included. Nothing changes for a provider or forrunStreamAgent.The client inlines images and text files into the request and drops every other file with a warning: right for a model, wrong for a backend that reads uploads itself, such as n8n’s Chat Trigger. Pair it with
rawFileInject: 'none'so the files are not also inlined into the message’s text. Set on a send only: a retry or an edit carries no files again.@aparte/core,@aparte/engine -
5e31fa9: A failed turn now shows a plain sentence chosen by the error’s code, in the reader’s language, instead of the transport’s or the provider’s raw message. The raw message goes to the browser’s console and stays on the error segment;
setErrorRendererstill receives it asmessage, and now also thesentence.Seven locale keys hold the sentences, and every language of
@aparte/localestranslates them:errorUnreachable(NET_OFFLINE, NET_ERROR),errorTimeout(NET_TIMEOUT),errorRefused(CONFIG_INVALID_KEY, a 401 or a 403),errorRateLimited(USAGE_RATE_LIMIT),errorTooLong(USAGE_CONTEXT_EXCEEDED),errorFailed(PROVIDER_ERROR, PROVIDER_UNAVAILABLE) anderrorNoAnswer(USAGE_BAD_REQUEST, PROVIDER_POLICY, PROVIDER_MISCONFIGURED, CONFIG_INVALID_MODEL, UNKNOWN_ERROR). The new codePROVIDER_MISCONFIGUREDnames a provider reached but set up so it cannot answer;@aparte/runner-n8nraises it for a workflow that is not published, waits for credentials, streams nothing, answers without a text field or answers through response nodes. “Try again in a moment” is said only where a moment can help.extendLocalechanges them.A tool whose handler throws now ends the turn as UNKNOWN_ERROR, whatever it threw: its own failed fetch or 401 no longer reads as the assistant being unreachable or the provider refusing the key.
An error keeps its own words when they were written for the reader or the developer already: the turn limit (MAX_TURNS_EXCEEDED, localized when it is raised), the two setup errors core raises before any request (CONFIG_NO_PROVIDER, CONFIG_MISSING_KEY), and an error segment an app or a plugin adds with a code of its own or none. A custom error renderer’s card is left alone on a language switch.
@aparte/core,@aparte/locales -
5e31fa9: Add
<aparte-launcher>: put your chat inside it and it floats as a button in the bottom corner of the window, opening the chat as a card above it, and as a full-window sheet on a phone.html<aparte-launcher label="Help"> <aparte-chat></aparte-chat> </aparte-launcher>The button is the button recipe (solid, primary, large): round with a speech bubble, or a pill when you give it a
label. A click opens the chat, and a second click, Escape, or any control of yours markeddata-aparte-launcher-closecloses it. Opening with the button hands the focus to the composer;show()andopenset from a script move no focus, so a page that restores the saved state on load does not steal it. Closing gives the focus back to the button when it was inside. A closed launcher keeps the chat mounted, so the conversation and the client stay, but out of sight, out of the tab order and out of the accessibility tree — whateverdisplayyour card sets, inline or by a rule.open,show(),close()andtoggle()drive it from a script, andaparte-launcher-togglereports every change — including, while the chat is open, the window crossing the breakpoint, so a host that takes its page out of reach under the sheet needs no media query of its own.Escape closes the launcher unless something inside has the key first: a control that answered it (a menu closing, the context gauge’s panel), an input method that is composing, or a modal dialog open in your card, which closes itself on it. The next Escape closes the launcher.
Under
breakpoint(30rem of window in width or in height unless you set a width,nonefor a card at any size) the open chat fills the window, Tab stays inside it, and the button moves to the top corner — or hides, when your card holds its own close control. The size, the offset from the edges, the gap and the corners are--aparte-launcher-width,-height,-offset,-gapand-radius.The locale gains
launcherOpenandlauncherClose, the button’s accessible name when it shows no text, translated in the nine languages of@aparte/locales. The icon provider gainsmessageSquare, the closed button’s glyph, and the button asks forlauncherOpenandlauncherClosefirst, so a theme redraws the button alone (registerIcons) without redrawing every close button in the chat. The button reads them from the chat inside it when that chat carries its own config (a wrapper’sconfigprop,attachConfig), and from the page’s config otherwise, so a French chat gets a French button without a global setting.On a server-rendered page, hold the launcher with
defer-hydration— on it, or on an element around it — and release it withreleaseDeferredHydrationonce your framework has hydrated, as the launcher page shows. The launcher draws its button when it connects, and hydration removes what it did not render; if something removes the button anyway, the element puts it back.The context gauge now answers an Escape pressed inside it on its own element, and stops and default-prevents it: an element around a gauge that listens for Escape no longer sees that press, which is what lets a launcher or a dialog around it stay open under the panel. An Escape from elsewhere still closes the panel and is left to whatever else listens for it.
The docs’ Support widget template is now built on it, in place of the launcher it drew by hand.
@aparte/core,@aparte/locales -
5e31fa9: Tool rows are quiet by default: the label shimmers while a call runs and nothing says “Done” once it ends. For the old row with a spinner, “Running” and “Done”, call
aparteGlobalConfig.registerToolDisplay('*', { quiet: false }).A wait for approval, a refusal, a failure and a stop still show their word and their note, and a screen reader still hears “Running” and “Done”.
quietkeeps its meaning: it was opt-in for a chat read by people, and it is now what a chat gets unless a display saysquiet: false.A display is used whole, so
quiet: falseon'*'reaches only the tools without an entry of their own. A tool that has one — a label, an icon, a summary fromregisterToolSummary, a formatter fromregisterToolInputFormatter, any field — saysquiet: falsein that entry to keep the agent’s row. Under reduced motion and in forced colours, where the shimmer cannot say a call is running, a quiet row shows “Running”.@aparte/core -
5e31fa9: New package:
@aparte/runner-n8nputs aparté on an n8n chat workflow (a Chat Trigger and its AI Agent), in place of n8n’s chat widget. PasscreateN8nRunner({ webhookUrl })asAparteClient’sstreamRunner;loadN8nHistory({ webhookUrl, sessionId })reads a conversation back after a reload.The agent, its tools and its memory run in n8n; each turn sends what n8n’s widget sends — the session, the person’s message, their files as a form — and the chat draws the answer, streamed when the Chat Trigger streams, whole when it waits for the last node. Two nodes that stream are drawn as two blocks, in order, never interleaved. A new conversation gets a new n8n session unless you give one, so two chats never share a memory.
The errors say what to change in n8n: an inactive workflow’s 404 with n8n’s hint, a trigger that streams while its agent does not, a last node that returns no
output/text/messagefield (where n8n’s widget printed the JSON into the bubble), credentials the workflow waits for, a memory not connected to the trigger when loading history, a workflow that answers through response nodes over n8n’s chat WebSocket, and a proxy that gave up (a 502/504 page, never shown as markup).The history leaves out a tool’s call and its result, as the live stream does — n8n’s widget drew them as bubbles after a reload, the tool’s result as raw JSON. The tests replay answers recorded from live n8n 2.35.7 and 2.44.6 instances, byte for byte, including a
keepalivethat opens the stream when a slow node runs first, and read them cut into any pieces. n8n’s stream carries the text of the answer only, so the agent’s tool calls are not drawn.@aparte/runner-n8n -
5e31fa9:
@aparte/locale-fris removed: importfrfrom@aparte/locales/frinstead, the same strings. The new package also translates the chat into Spanish, Portuguese (Brazil), German, Italian, Russian, Japanese, Korean and Chinese (Simplified).diff- import { fr } from '@aparte/locale-fr'; + import { fr } from '@aparte/locales/fr';This removal skips the notice release the stability page promises for a frozen name:
@aparte/locale-frleaves in this release, without a release that only deprecates it. Its last version, 0.26.0, stays installable on npm and gets no further release; it lacks the strings 0.27.0 adds (the error sentences, the launcher’s names), which a French chat that keeps it shows in English.Each language is its own entry (
@aparte/locales/es,@aparte/locales/pt-BR,@aparte/locales/zh-CN…), so a page loads only the one it imports; all nine are also named exports of the package root (fr,es,ptBR,de,it,ru,ja,ko,zhCN). Every one translates every string core draws, keeps English’s placeholders in the same keys, sets atagthatIntlformats dates and numbers in, and is written left to right.check:locale-keysholds every language of the package to core’s list, as it held French: a key core adds and a language leaves out fails the gate instead of showing in English on a translated page.The eight new languages were translated for this release and have not yet been reviewed by native speakers; corrections are welcome as issues or pull requests.
@aparte/locales
Patch Changes
-
5e31fa9: A file outside the
acceptlist of<aparte-composer-add-attachment>is no longer attached when it is dropped, pasted, or picked through the dialog’s “All files”. It is left out without a message.acceptonly reached the file input, where the browser reads it as a suggestion: its dialog offers “All files”, and a drop or a paste never sees the list at all. Measured against a real n8n: withaccept="image/*", a PDF dropped on the composer was attached and sent. The list is now checked where the file arrives, read the way the browser reads it —.extagainst the name,type/*against the MIME family, anything else against the exact type.@aparte/core -
5e31fa9: An attachment tile now shows its file name on a touch screen all the time, and on keyboard focus where the tile takes it — a pending file through its ✕, a picture the app opens previews for — not only on hover. A sent file tile that is not a control still shows it on hover and on touch.
The tile’s label shows only the extension, so two PDFs looked alike to anyone who could not hover. The name keeps its scrim over a picture. On a touch screen, where it is always shown, the sent strip’s tiles grow to 56px and the composer’s to 72px, the name’s band is tighter, and a sent file’s extension sits above it, so the name covers about 40% of a sent tile instead of 79% and no label hides under a pending file’s ✕.
@aparte/core
Version-only bumps (no changes of their own): @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/docs-mcp.
0.27.1
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
061b2d5: The copy button is off by default: a message carries no button until you turn one on. To keep it, call
aparteGlobalConfig.setBubbleActions({ copy: true }).APARTE_DEFAULT_BUBBLE_ACTIONS.copyis nowfalse, so every action is off until an app asks for it. Copy is the one core honours on its own, and it still works the same once on, hidden while a turn has no text to copy. A help widget or a support chat is read rather than quoted, and a button under every answer was noise there; a chat product that wants it adds it in one line. An explicit per-role list (setBubbleActions({ assistant: ['copy', 'retry'] })) is unchanged: namingcopythere turns it on.@aparte/core
Version-only bumps (no changes of their own): @aparte/engine, @aparte/runner-ag-ui, @aparte/runner-n8n, @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/locales, @aparte/docs-mcp.
0.27.2
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
8b81036: A file in a sent message can open: declare
setHostHandlers({ filePreview: true })and its tile sendsaparte-attachment-preview, which now carries the file’stype.Images keep their own declaration,
attachmentPreview, so a lightbox written for images is never handed a PDF. A file tile becomes a button only when its URL is one a tab can open — the composer’s object URL, or a remote URL; adata:URL stays a plain tile. The event’stypeis the MIME type (''when unknown) on every preview, the image ones included.@aparte/core -
14c782d: A workflow that answers through response nodes now works: the runner reads n8n’s chat WebSocket, and a Chat node’s buttons are answered in the page after
setupN8nButtons().On a Chat Trigger set to “Using Response Nodes”, n8n answers the first message with an execution to listen to; the runner opens n8n’s chat socket for it, draws each message the workflow’s Chat nodes send, and keeps the socket while the execution waits — a written reply to a Chat node that asks for one goes on it. A Chat node that waits for an approval sends buttons: the turn pauses on them as a call the page answers,
setupN8nButtons()asks them at the composer with their own labels, and the chosen button resumes the execution. Only buttons whose links lead back to the same n8n are shown. n8n 2.x runs response nodes only on a Chat Trigger in “Hosted Chat” mode. A runtime without a globalWebSocketpasses one as the runner’sWebSocketoption.@aparte/runner-n8n
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/locales, @aparte/docs-mcp.
0.27.3
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
0689e87: An approval can now require one of its options: pass
freeText: falsetorequestUserInputand the “Or tell it what to do instead” field is left out.The corner still declines, since an unanswered approval is never a yes. The default is unchanged: the field is there unless a request says otherwise.
@aparte/core -
a329d19: n8n’s “Block User Input” is now honoured: a Chat node’s buttons are asked without a field to write in when it is on.
Without it, the person may still write instead of choosing, which n8n reads as a message and takes as not approved, as with its own widget.
@aparte/runner-n8n
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/locales, @aparte/docs-mcp.
0.27.4 Latest
Every @aparte/* package ships at this version (they are released in lockstep).
Patch Changes
-
7db31fc:
freeTexton an approval request is renamedallowOther, the name a question’s free-text option already has. ReplacefreeText: falsewithallowOther: false.Published in 0.27.3 an hour before: one idea, “may the person answer in words”, now has one name on both panels. Same default (
true), same meaning;setupN8nButtonspasses it for n8n’s Block User Input.@aparte/core,@aparte/runner-n8n
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/locales, @aparte/docs-mcp.