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 icongetIcon('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
toporbottomtooltip (theaparte-tooltiprecipe) 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
- 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 (
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.setImagePolicyallows where it comes from. The names the beta marked deprecated are removed. ACompactionTargetneedsupdateMessage. - 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
AparteLocaleand 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/coreasks 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
- What most apps change. A picture a model or a tool chose waits for a click unless
-
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-compactioncompacts by itself once it is set up (past 80 % of the window by default).- The
compactionSummaryTitlelocale 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/localesno longer carry it. - The
windowfield of anaparte-compactrequest’s detail, read “for one more release” since 0.26.0: givesetupCompactionacontextWindowinstead, or let the model declare its window. - The viewport’s
clearMessages(), deprecated since 0.4.0: callclearAll(), which also removes the bubbles.clearMessages()on<aparte-chat>, on the framework components and on the imperative API is unchanged. AparteMessageBranchand thebranches/activeBranchIndexfields ofAparteMessage, deprecated since 0.4.0 and read by nothing: branches are the message tree (addSiblingOf,navigateBranch,aparte-path-changed).AparteCodeSegment.showLineNumbersandAparteErrorSegment.stack, deprecated in 0.19.3 and read by nothing: put a stack indetails.@aparte/core,@aparte/locales
-
67efebe: A picture a model or a tool chose no longer loads by itself:
data:andblob:pictures do, and any other shows as a link naming its host until the reader clicks it. To load your own, callaparteGlobalConfig.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:
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 (), 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.sanitizeHtmlapplies the policy after whichever sanitizer is set, so DOMPurify or your own keeps it, and@aparte/plugin-streaming-markdownapplies it before anysrcis set, so nothing is fetched while a reply streams. A picture’s own address decides; of itssrcset, 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-cardsdraws 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 throughctx.image(src, props)andctx.loads(url). - The rule.
setImagePolicytakes a list ('self','*', origins) or a function(url) => 'load' | 'hold' | 'drop'.config.decideImage(url)answers for a plugin that draws pictures itself, andconfig.showImage(url)records the reader’s choice. Every picture that loads goes withreferrerpolicy="no-referrer". A CSPimg-srcis the stronger rule beside it: it also checks where a redirect leads. - Two locale keys,
imageLoadandimageLoadNamed, name a held picture;@aparte/localescarries 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
- 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:
-
f5080e9: A
CompactionTargetmust haveupdateMessage, and the type now says so;clearAllandappendMessageleave it. Thewindowof anaparte-compactrequest is no longer read: setcontextWindow.Since 0.26.0 a compaction writes one checkpoint through
updateMessageand 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. Thewindowa 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 atarget="_self"on it is ignored.On an
httpspage that URL resolves tohttp://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 withhttp:orhttps:is now external, in the built-in sanitizer and in the live streamed link of@aparte/plugin-streaming-markdown. A same-site link spelledhttps:/pathgoes to a new tab too, which is harmless.@aparte/core,@aparte/plugin-streaming-markdown -
0a6dd91: A segment streamed with
appendToSegmentnow 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
isStreamingflag 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’sstreamingattribute, 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
suggestionsblock now sends the text on its button: apromptin a{ "label", "prompt" }reply is ignored. A card’sactionsnow receives who wrote the card asactions(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 itsprompt(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 thesuggestionsattribute or property of the element instead: a page’s ownsuggestionskeeps its free label/prompt split.@aparte/plugin-cards—actionstakes a second argument,{ origin }, and theaparte-card-actionevent carriesdetail.origin:'tool'for the result of a tool your app runs,'segment'for acardsegment your app added, and'fence'for a card the model wrote as acard-<name>code block in its answer. A card of the model’s own ('fence') draws only the links among its actions: an action carrying anidis not drawn, since youraparte-card-actionlistener would run it with a label and data the model chose; the buttons you give it throughactionsare drawn. Asendbuilt 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 anidand decide in the listener. The guide’s example no longer buildssendfrom 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 shortllms.txtthat 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-idordata-rolechanges 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 tooldetailsstring or a custom sanitizer’s output relied on those attributes (below), or if a reply was meant to carry a text field or arolesuch astextbox.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-paneordata-aparte-launcher-closeis 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/corecomponent pages for the dialog, sidebar, split and launcher say so. - A tool display’s
detailsstring goes through the sanitizer, sodata-action,data-segment-id, the reserved attributes below and a widgetroleare 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: everydata-aparte-dialog-*,data-aparte-sidebar-*anddata-checkpoint-*, plusdata-aparte-chat,-host,-theme,-suggestions,-split-pane,-launcher-close,-bubble,-empty,-panel,-panel-host,-card-part,data-segment-idanddata-action. Otherdata-*attributes (data-line,data-aparte-openfor 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 anddatasetreads) so a new attribute cannot be forgotten.@aparte/core— a widgetrole(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 istype="checkbox"(a GFM task list); a kept checkbox is rewritten totype="checkbox"anddisabled, so a paddedtype="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 reserveddata-*, widget roles,aparte-classes, layout styles, ids) and keeps the same task-list checkbox, rebuilt as<input type="checkbox" disabled>(checked or not, with itsclass), 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.resolveConfigskips a[data-aparte-host]element that noattachConfigbound, so content inside one keeps the chat’s own config instead of the page’s. The sidebar and split no longer stamparia-expanded/aria-pressedon such text.@aparte/plugin-cards— a card’ssendbutton 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 honoursdata-action="download"only on its own button, not in a converter’spreviewHtml.@aparte/core— without a DOM parser (Node, SSR, a Worker) the sanitizer is a single linear pass: a stray<comes out as<, 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'; 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— asrcsetmade 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
- A control carrying
-
048864d: The agent and chat handlers now refuse a request whose Content-Type is not
application/json(415), andprepareRequestcan now remove a server tool for good.If you call
createAparteAgentHandlerorcreateAparteChatHandlerfromAparteBackendTransport,createAgentStreamRunneror anyfetchthat setsContent-Type: application/json, nothing changes. A caller that posts the JSON body without that header (a barefetch(url, { body: JSON.stringify(...) })sendstext/plain) must add it. Acontent-typeyou pass in theheadersoption ofAparteBackendTransportorcreateAgentStreamRunner, in any case, replaces the library’s one instead of being joined to it (application/json, application/jsonwas refused). The check runs beforeauthorize, and it closes a cross-site hole: an HTML form withenctype="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
allowedOriginson both handlers: a request carrying any otherOriginheader is refused with 403, beforeauthorize. A request with noOrigin(a server, curl) is left toauthorize. It is off by default. For a cookie session, also set the cookieSameSite=LaxorStrict. The server-loop and backend-transport guides say so. createAparteAgentHandlerruns a tool only if the request returned byprepareRequeststill lists it. A tool you remove fromrequest.tools(sayadmin_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
onTicketsingle-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
- New option
-
99784ad:
setMessagesdraws 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>/chatat the end ofwebhookUrl(https://site/n8n/webhook/abc/chattakes/n8n/webhook-waiting/…; a base path that itself holds a/webhooksegment 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 whosewebhookUrldoes not end in/webhook/<id>/chat, sets thewaitingPrefixoption ('/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,onWarningis told (and nameswaitingPrefix), 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 fieldsexecutionStarted,executionId(a number) andresumeToken. 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
errorframe on the socket now fails the turn with “The n8n workflow failed.”; the frame’s message is passed toonWarninginstead of being shown.The same holds for an
errorframe in the HTTP answer or stream: the turn fails with “The n8n workflow failed.” and the frame’s message goes toonWarning. 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 abeginframe 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 inlinedata:image/…picture still draws.@aparte/plugin-mermaid— Mermaid loads a node’simg:and a sequence actor’s icon in the page while it draws, before the plugin turns the SVG into an image, andsecurityLevel: '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 notdata:image/…, however the source spelled it (a YAML anchor and alias, an escape,properties, ordetailsreading an element of the page). So a sequence diagram that only says “properties” in a message, or a label that says “img”, is drawn. Amermaidobject withoutmermaidAPI(your own, passed tosetupMermaidWith) falls back to refusing on the text, which refuses those too. A style a diagram writes is read as well: a CSSurl(,image-set(,cross-fade(or@importanywhere 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 asrgb(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’stextFontFamily…) that holds more than a font name is dropped,textFill/textStrokemust be colours, andimage-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. Acursor(and a dataZoom’scursorGrab/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-allowedand the other keywords are kept. A symbol writtenemptyImage://…is dropped likeimage://…(ECharts stripsemptyand 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 owntooltip.callbacksor inoptions.interaction(andexternal,filter,itemSort, an unknownposition) is dropped like the chart’s top-level tooltip ones; it threw on every hover. A tooltip’s owninteractionandpluginskeys 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 thethemeCSSit renders with, after anythemeCSSyou 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 owndompurifyConfig, which covers every such label rather than the one diagram type.@aparte/plugin-mermaid -
574884c:
createAparteChat()andcreateConversationManager()are no longer marked deprecated: they stay, for the Svelte 4 component at the package root. On Svelte 5, start from@aparte/svelte/runesas 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-markdownalone 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 wasDone: **Bookshowed 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
isStreamingflag, and a segment’ssetupread that as finished, because a restored message’s segments carry none either. Now a text or reasoning segment flaggedisStreaming: trueis written by the streaming renderer from its first chunk;addSegmenton a streaming bubble treats an unflagged one as arriving; andpopulateBubbleFromMessageflags the last segment of a message whose status isstreaming. A segment set by hand withsetSegmentsis 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 toshowPanel()gets the same behaviour on its one-line fields; a field that handles Enter itself and callspreventDefault(), 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.