0.25.0
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
0d50cfd: Server-rendered pages now hydrate cleanly: an aparté element waits while it, or an ancestor, carries
defer-hydration, and draws itself whenreleaseDeferredHydration(root)removes the attribute.Core’s elements draw themselves when they connect (light DOM: their children, their attributes, a style on the chat’s root). On a server-rendered page that happened as soon as
@aparte/coreloaded in the browser — defining the elements upgrades the ones the server sent — so the framework then hydrated a DOM it had not rendered: React threw error 418 and discarded the server’s HTML, measured in a Next production build. A held element does nothing until it is released; its upgrade is then replayed as the browser would have run it (the observed attributes it has, thenconnectedCallback). An element removed while held is never torn down, and replays its attributes when it connects later. Removing the attribute any other way releases a microtask later — or at once, whenreleaseDeferredHydrationis then called on that root, as a wrapper does after a hydration that dropped the attribute.The wrappers render the attribute on their root and release it themselves. For server-rendered elements of your own, put
defer-hydrationaround them and callreleaseDeferredHydrationonce your framework has the page; an element class of yours that draws itself on connect honours it afterdeferHydrationOf(MyElement), called beforecustomElements.define. Without the attribute nothing changes. It is the Web Components Community Group’sdefer-hydration, the one Lit’s server rendering follows, widened from an element to the elements under it.@aparte/core -
02351e4:
AparteChatHost.takeEcho(list)tells a wrapper whether a list the parent hands back is the host’s own report coming back, or the parent’s word.Call it before applying the list a two-way binding gives you. It answers true for a report the parent has not handed back yet — however late it lands — and takes it with every older one; for any other list it answers false and clears them, so a list the parent restores later is applied. The four first-party wrappers use it; a wrapper of your own over
AparteChatHostgets the same rule.@aparte/core -
908340b: Draw a tool call or a segment with your own component:
defineMountedToolRenderer(mount)mounts it, updates it and unmounts it with the element, andaparteGlobalConfig.overrideToolRenderer(match, renderer)— a method on anyAparteConfig— returns the function that takes it off again.defineMountedToolRendereranddefineMountedSegmentRenderer(type, mount)turn a mount function into a complete renderer: core creates the host, calls the mount once when the host is in the page, forwards every change of the call to the returnedupdate(never a remount, so a component keeps its state when the result arrives) and callsdestroywhen the host leaves for good. A card receives oneAparteToolCallView(toToolCallView):id,name,input,status,result,structuredResult,deferred,refusal,abortCauseand the segment. New types:AparteToolCallView,AparteMountedView,AparteToolMount,AparteSegmentMount.The registries are live: registering, unregistering or overriding a tool renderer — or a segment renderer — redraws the calls (or segments) already on screen that it now draws, and only those; the renderer that drew them gets its
cleanup.config.overrideToolRendererand the standaloneoverrideSegmentRenderer(renderer, config?)stack: the newer declaration wins, taking off an older one leaves it, and taking off the last restores what was there before — core’s own renderer, for a built-in segment type likecode. The redraw happens in a microtask, gathered per config (ten components declaring cards in one render cost one pass; an override taken off in the same tick costs nothing), reaches a chat with its own config when the global registration it inherits changes, and follows a chat whose own config is attached after its first rows. A rendering-version bump — a Markdown or highlight provider arriving late — keeps the cards a mounted renderer holds mounted, and so does the end of a turn:setSegments()with the same segments (the host re-syncs the finished message) hands those cards the final state instead of drawing them again. A restore function does nothing after the config’sreset(), and leaves a registration made directly since in place.@aparte/core -
986df98: Unmount what you mounted in a segment or a tool call in
cleanup(element, segment): core now calls it, once, when the element leaves for good.AparteSegmentRenderer.cleanupwas declared and never called; it is called now, with the segment’s latest state, andAparteToolRenderergains the same hook. It runs when the element is replaced by a newrender(), when its segment is removed, when its message is redrawn in a new frame, and when its message leaves the page — but not forupdate(), not forrelabel(), and not for a message a framework only moved (a bubble taken out and put back before the next microtask keeps its elements). A bubble that comes back after a longer absence draws its segments again. A renderer that replaces its own element insideupdate()cleans up what it replaced; the bubble follows the new element.A tool call is updated, relabelled and cleaned up by the renderer that drew it, recorded when the row is set up — not by whichever renderer is registered at that moment, which could hand core’s own row, or another renderer’s markup, to an
update()that never drew it.@aparte/core -
de8732e: A runner of your own now receives the tools the request allows and its
toolChoice:input.tools(each aRunnerTool—name,description,inputSchema) andinput.toolChoice('auto','none'or{ name }).Core sends
toolswhen the model declaresfunction_calling, and the provider used to post the worker only the messages and the generation options, so a runner that emitstool_usehad to hard-code its tool list — offering a small local model tools the app never enabled, and unable to honourtoolChoice: 'none'or a forced tool. Both are absent when the request has none. The built-in runners take neither, and still warn when a turn needs a tool call they cannot carry. Closes #119.@aparte/provider-transformers -
7d9550d: Svelte 5 apps now need Svelte 5.20 or later (peer
^4.0.0 || ^5.20.0, on@aparte/svelteand on@aparte/plugin-model-selector’s optional Svelte peer); on Svelte 5, import from@aparte/svelte/runes;createAparteChatandcreateConversationManagerare deprecated, and 0.26.0 moves today’s package root to@aparte/svelte/legacy.@aparte/svelte/runesis the chat written in Svelte 5:<AparteChat>with snippets (emptyState,aboveComposer,composer,toolbar,bubble), a bindablemessages, callback props (onmessageSent,onaction,onmessagesChange,onmessageAppended,ontypingChange,onconversationCreated), a stable id from$props.id()and a server render that SvelteKit hydrates;<AparteUi>;createConversations()/AparteConversations, the saved conversations as reactive state;createAparteClient; and<AparteToolUI>/<AparteSegmentUI>. It needs Svelte 5.20 or later, and the peer range is now^4.0.0 || ^5.20.0for the whole package, the root entry included — and on@aparte/plugin-model-selector’s optional Svelte peer too, which follows the wrapper’s: an older Svelte 5 that is installed now failsnpm installwith ERESOLVE, even for an app that only uses the root component or the plugin.createAparteChat(usebind:messages) andcreateConversationManager(usecreateConversations()) are deprecated. In 0.26.0,@aparte/sveltebecomes the runes component and today’s — the Svelte 4 syntax component and these two helpers — moves to@aparte/svelte/legacy, where it keeps working on Svelte 4 and 5.@aparte/plugin-model-selector,@aparte/svelte -
cf0590e:
@aparte/angularinstalls on Angular 20 and 21, not only 19.The peer range is
^19.2.0 || ^20.0.0 || ^21.0.0. The package is still compiled on 19.2 in Angular’s partial mode, which Angular guarantees a newer application can link, so one build serves the three majors. What makes the range a fact rather than a hope: the specs run on 19 (with zone.js, zoneless, and in a server render), and the repo’s Angular example now runs on 21, zoneless, driven in a real browser by the end-to-end suite. Angular 19 is past its upstream support window and leaves the range at 1.0. Angular 22 joins once it is exercised the same way.@aparte/plugin-model-selectorfollows: its optional@angular/corepeer (for the./angularentry) takes the same range, since a narrower one would failnpm installon Angular 20 and 21 even for an app that never imports that entry.@aparte/angular -
c367c85: Bind
[conversations]on<aparte-conversation-list>with noCUSTOM_ELEMENTS_SCHEMA: its directive now has that input.The element directives were generated from the elements’ attributes only, so a member that exists only as a JS property — the list’s rows — had no input, and
[conversations]="…"did not compile unless the component switched template checking off for every unknown tag. The generator now also emits an input for each public property an element has no attribute for; it hands the value over as the property.<aparte-conversation-list>’sconversationsis the only such member today.@aparte/angular -
d6cf93b: Set
AparteChatComponentinputs from code withcomponentRef.setInput(): they are signal inputs now. Templates do not change, and the per-message template can be written as content:<ng-template aparteBubble let-message>.Every input keeps its public name (
[messages],placeholder,disabled,centerWhenEmpty,[bubbleTemplate]…), so a template binding works as before; what changes is the class member behind it, which is an AngularInputSignalrather than a writable property. Code that created the component and assignedinstance.config = …sets it withcomponentRef.setInput('config', …)instead, which is how Angular sets any input from code.AparteBubbleDirectiveis new:<ng-template aparteBubble let-message>written inside<aparte-chat>draws each message, the same as passing aTemplateRefto[bubbleTemplate], which still wins when both are given.[(messages)]two-way binding is supported and tested: the chat reports its own changes throughmessagesChange, and a write from the parent emits nothing back.The component renders on the server: the host that drives the custom elements is created after the first render in the browser only, and nothing touches
requestAnimationFrameduring a server render. It also runs under zoneless change detection (the default for a new app since Angular 21); both are covered by specs.@aparte/angular -
15254e4: Draw a tool call with your own component:
<AparteToolUI name="…">(React, Vue, Svelte 5) or<ng-template aparteToolUI="…">(Angular).The card renders from where you declare it, so it reads your app’s contexts — React context through a portal, Vue
injectthroughTeleport, SveltegetContext(the snippet is mounted with the declaration’s contexts), Angular DI (the view is created where the template is declared, and it works zoneless). It is mounted once per call and updated in place when the call changes, so a component keeps its local state when the result arrives. It is unmounted when the call leaves the page or when the declaration unmounts, and the call goes back to core’s row; a declaration made while calls are already on screen takes them over. Of two declarations for one tool the newer wins, and removing the older keeps it. A server render declares nothing and draws nothing: the cards appear in the browser.AparteSegmentUI(<ng-template aparteSegmentUI="…">withAparteSegmentUIDirectivein Angular) does the same for a segment type. In Svelte both are in@aparte/svelte/runes. Vue also exportsisAparteElement, the test to givecompilerOptions.isCustomElement: it accepts theaparte-*elements, and leaves the wrapper’s own component names to Vue, so a kebab-case<aparte-tool-ui>gets Vue’s warning instead of becoming an inert element (write it<AparteToolUI>).@aparte/angular,@aparte/react,@aparte/svelte,@aparte/vue
Patch Changes
-
9bc30ea: Stopping a client that never started no longer touches
window, so it is safe on a server.The constructor prepares the send handler without listening, and
stop()only returned early when there was no handler — so it always removed listeners fromwindow. On a server that is a ReferenceError; a SvelteKit page that started a client the documented way failed with a 500 (Svelte runsonDestroyat the end of a server render). A started client still removes its listeners onstop(), once.@aparte/core -
6fed78c:
<aparte-approval-mode>and<aparte-model-selector>now wait for a server-rendered chat to hydrate before they draw.Placed in the toolbar of a server-rendered
<AparteChat>, either tag drew itself as soon as its plugin was imported — before the framework had hydrated the page — so React found children it had not rendered and threw the server’s HTML away (error 418). Both now honourdefer-hydration, like core’s own elements, and draw when the wrapper releases the chat.<aparte-ask-user>already did, through the<aparte-elicitation>it extends. An element of your own that draws itself on connect can do the same: calldeferHydrationOf(YourElement)beforecustomElements.define.@aparte/plugin-approval,@aparte/plugin-model-selector -
cf0590e:
@aparte/angularinstalls on Angular 20 and 21, not only 19.The peer range is
^19.2.0 || ^20.0.0 || ^21.0.0. The package is still compiled on 19.2 in Angular’s partial mode, which Angular guarantees a newer application can link, so one build serves the three majors. What makes the range a fact rather than a hope: the specs run on 19 (with zone.js, zoneless, and in a server render), and the repo’s Angular example now runs on 21, zoneless, driven in a real browser by the end-to-end suite. Angular 19 is past its upstream support window and leaves the range at 1.0. Angular 22 joins once it is exercised the same way.@aparte/plugin-model-selectorfollows: its optional@angular/corepeer (for the./angularentry) takes the same range, since a narrower one would failnpm installon Angular 20 and 21 even for an app that never imports that entry.@aparte/plugin-model-selector -
77d7493:
<aparte-ui>no longer builds its element during a server render, so a hydrated Angular SSR page shows it once.It built the element in
ngAfterViewInit, which Angular also runs on a server: the element was serialized into the HTML without its props or listeners, hydration kept it, and the browser appended a configured one beside it — two model selectors, two counters. It is now built in the browser only, the way React, Vue and Svelte build theirs in an effect oronMount.@aparte/angular -
aa0a6d8: Under
[(messages)], a message the chat appends right after one of its own changes is no longer dropped when the parent’s field comes back through[messages].The chat reports a change (
messagesChange), the two-way binding writes it into the parent’s field, and the chat then appends without reporting — an optimistic assistant placeholder, or core’s append right after a flush of buffered chunks (an edit, a retry). At the next change detection the parent’s field came back through the input and reset the chat to the list it had reported, so the placeholder vanished and the streamed reply landed on the user’s message, or nowhere. The chat now recognises its own reports that have not come back yet and keeps its list when one does — the rule React, Vue and Svelte apply — while a list you restore later is still yours.@aparte/angular -
ef17513: A server-rendered
<AparteChat>now hydrates without a mismatch, in Next, Nuxt, SvelteKit and Angular SSR.The server’s HTML carries
defer-hydrationon the chat’s root, so core’s elements leave it alone until the framework has the page; the wrapper then releases them, before it builds the chat’s host. Before, the elements drew themselves as soon as@aparte/coreloaded and the framework found a DOM it had not rendered — React threw error 418 and discarded the server’s HTML. An app with no server render holds nothing, so a slot’s component can talk to the elements it renders from its own mount hook: React reads “am I hydrating?” throughuseSyncExternalStore, Svelte draws the attribute on the server only, Angular — where it is on the<aparte-chat>host itself — binds it only while hydrating a host the server sent with it, and Vue releases from the root’s ref. On a server-rendered page, Vue’s ref still releases before anyonMounted; in React, Svelte and Angular a slot’s component mounts before the chat does (its effect,onMountorngAfterViewInitruns first), so on that first load it finds the elements live only once the chat has mounted.@aparte/angular,@aparte/react,@aparte/svelte,@aparte/vue -
02351e4: A list you assign back to the chat — a reopened conversation, an undo — is now drawn, even when the chat reported it to you earlier.
Under a two-way binding every list the chat reports comes back through your prop, sometimes late — an earlier report after its next change — and applying that echo would roll back what the chat did since without reporting. To tell an echo from your own word, the wrappers remembered every list the chat had ever reported, so a list you kept and assigned back later was taken for an echo and ignored: the chat went on showing the other conversation, and its next report wrote that conversation’s messages into the one you had reopened. Only the reports you have not handed back yet count now; any list the chat did not report clears them. React, Vue, Svelte (both entries) and Angular apply the same rule.
@aparte/angular,@aparte/react,@aparte/svelte,@aparte/vue -
2051a75:
createAparteClient()no longer fails a server render, and a server render no longer leaves itskeyResolveron the global config.It stopped the client in
onDestroy, which Svelte runs at the end of a server render too, and stopping a client that never started readwindowthere and failed every SvelteKit page that followed the docs.stop()now touches nothing of the browser for a client that never started (@aparte/core), so the stop stays inonDestroy— the one hook a server render runs — and takes back the key source the client put on the config when it was constructed. Moved to a hook a server never runs, each request would have added one to the config the whole process shares, holding whatever that request’s resolver closed over.@aparte/svelte -
bf7de83:
AparteChatandAparteUiare typed for Svelte 5 as well as Svelte 4: props, events and slots check under either.The package is now built with Svelte 5’s tooling, which emits each component’s type in a form both majors read (a Svelte 4 class constructor and a Svelte 5 component function). Verified with the packed tarball installed in two fresh projects, Svelte 4.2 with
svelte-check3 and Svelte 5.57 withsvelte-check4, both withskipLibCheck: false: the documented usage checks clean, and an unknown prop or a wrong prop type is reported in both. Nothing changes in the components themselves.@aparte/svelte
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-artifacts, @aparte/plugin-ask-user, @aparte/plugin-compaction, @aparte/plugin-marked, @aparte/plugin-shiki, @aparte/plugin-streaming-markdown, @aparte/plugin-titler, @aparte/locale-fr, @aparte/docs-mcp.
0.26.0 Latest
Every @aparte/* package ships at this version (they are released in lockstep).
Minor Changes
-
f0b38dd: A page that registers a compactor now has its sends wait for it, and a request the model refuses as too long is compacted and sent once more. Nothing happens without a compactor: with none registered (
setCompactor), every send, retry, edit and resume behaves as before. With@aparte/plugin-compactionset up, the composer shows Stop while a summary is being written before a send, and an overflow that used to end as an error card now ends as an answer, with the separator above the kept turns as the only sign.What is new, all additive:
- Before each request goes out, the client calls
compactor.beforeSend({ target, targetId, exclude, requestTokens })onceaparte-message-startis dispatched (so Stop is showing and the reply is waiting) and the request is built (files inlined).excludeis the user message and the reply of the turn in preparation;requestTokensis the estimate of the request. When it resolvestruethe client builds the request again, from the history as it now is. A Stop while it waits ends the turn as aborted, the way a Stop ends one today. A compactor that throws there is warned about and the request goes out as built. - When the model refuses a request with
USAGE_CONTEXT_EXCEEDEDbefore the reply has any text or tool call, the client callscompactor.compact({ force: true, reason: 'overflow', exclude })and, when it compacted, sends the turn once more into the same reply. Once per turn. If there is no compactor, the compaction skips or fails, or the reply already said something, the error stands exactly as before. - The reply records
contextEstimate, the estimate of the request it was sent with, before each send, so the context measure can calibrate the character heuristic against the usage the provider reports. A request that carries an image records none, since the heuristic cannot read it whole. - Both calls carry
signal, the turn’s abort signal, so a programmaticabort()orstop()reaches a compactor that does not listen onwindow. - A retry or an edit under a framework wrapper rebuilds its request from the live transcript, so it carries a checkpoint written while it waited; the wrapper’s root now offers
getMessage, as the viewport does. AparteFormatAdapter.classifyError?(status, body)lets a provider name a failure a status cannot (an overflowing prompt is a 400). Core’svendorErrorcalls it for a direct call;createAparteChatHandlercalls it on the server and forwards the code aserror.code, still without the vendor’s prose, and the backend transport readserror.codewhen it is anAparteErrorCodevalue. A vendor’s own code (context_length_exceeded) is not one and is ignored on the client.- The server loop keeps the overflow code too:
createAgentHandlersends the code an error carries in itsrun-errorline (AGENT_RUN_FAILEDonly when it carries none), andcreateAgentStreamRunnerthrows a code that is anAparteErrorCodevalue as the error’s own code, so a server-side overflow is compacted and sent again like a direct one. A code that is not one of ours staysPROVIDER_ERROR, with the raw value indata.code. createAparteChatHandlerno longer relays the prose of anerrorevent the vendor streamed after its 200. The browser getsThe vendor reported an error.and the code when it is anAparteErrorCodevalue; the original goes to the server’s log.- The stream
errorevent takes an optionalcode(AparteStreamEventMap), andAparteError.fromkeeps a valid.codeof the error it wraps, so a code a provider streamed after a 200 reaches the client.
Why. Compaction that only runs after a turn cannot help the turn that overflows, and a send that races a summary carries the history the summary is about to replace. Classification stays in the provider because what a server writes for an overflow is its wire format, not core’s to know.
@aparte/core - Before each request goes out, the client calls
-
37b82d9: Register what compacts the context with
aparteGlobalConfig.setCompactor(compactor | null), and read it back withgetCompactor(). The separator’s Retry and the gauge’s Compact now use it. Nothing for you to change.What is new, all additive:
AparteCompactor,AparteCompactorRequest,AparteCompactorOutcomeandAparteCompactorReason(types). A compactor hascompact(request)(never rejects: the outcome saysok),settled(targetId?), and optionallybeforeSend(request)andpreview(request). A request names the chat by its element (target) and, when it has one, itstargetId, because a chat inside a shadow root is not found by id;force,excludeandreason(manual,retry,overflow,auto) say how it is asked.- A chat reads the compactor of its own config first (
resolveConfig(chat).getCompactor()), then the global one.setCompactornotifies like every other registration, so a mounted gauge hears that Compact now became possible. - The separator’s Retry asks the compactor when one is registered, with
force: trueandreason: 'retry', so it runs even when automatic compaction has stopped after failures. With none registered it dispatchesaparte-compactas before.
Why. Core cannot import the compaction plugin, and an event answers nothing: the caller needs to know the summary is written before it goes on, and a button needs to know it is still working.
@aparte/plugin-compactionregisters itself here when it is set up; core never compacts on its own.@aparte/core -
39c3d7f: A message can now carry a checkpoint,
contextSummary: from it on, the model reads the summary in place of every message before it, and nothing before it is deleted. The client sends the checkpointed history on send, retry, edit and resume. Nothing changes for a conversation with no checkpoint.What is new, all additive:
AparteMessage.contextSummary?: AparteContextSummary({ summary, count, createdAt }, exported as a type). It sits on the first kept message, which is ausermessage, so the history still alternates its roles.AparteMessage.contextEstimate?: number, the estimate in tokens of the request that produced a reply. The client writes it on each reply when it sends (none when the request carries an image the estimate cannot count), and the context measure reads it back to calibrate itself.historyFromCheckpoint(path)returns{ summary?, messages }: the messages from the LAST checkpoint on the path, and its summary. A legacycompaction: truemessage before that checkpoint is dropped with the rest. With no checkpoint the path comes back unchanged. It is exported from both entries, so a server or ahistoryfunction can use it.applyRetentionkeeps a checkpoint the trim cuts past: the new first message inherits the latest checkpoint’scontextSummary(same summary and date,countraised by the messages cut after it), in the flat list and in the branch tree. Before, a retained conversation lost both the originals and their summary.
What a host that builds its own history should know. A
historyfunction still receives the full transcript, checkpoint and everything before it; to honour a checkpoint, run the messages throughhistoryFromCheckpointand send itssummaryfirst (the client sends it as ausermessage under the same preamble acompaction: truemessage gets). The default'viewport'history does this itself.Why. Compaction used to replace the summarised turns with a summary message, so the turns, their attachments and their branches left the screen and the store. A checkpoint turns compaction into a view over a kept history: only what goes to the model is shortened.
@aparte/plugin-compactionwrites the checkpoints, and the bubble draws a separator above the message that carries one.How the tree behaves, so a host is not surprised. A checkpoint is a node shared by every path through it. Editing the checkpoint message edits that node in place and keeps the field. Editing a message above it clears everything after the edited message, the checkpoint included, so the full history is sent again. A retry of a reply above the checkpoint forks before it and sends no summary: it would describe messages the model is also reading.
@aparte/core -
37b82d9:
auto-compacton<aparte-context>is deprecated. It still works, and your page keeps compacting as before, but it will be removed in a future major version:@aparte/plugin-compactionnow decides when to compact (after a turn, and before a send that would overflow), so the gauge no longer needs to ask. Remove the attribute once your page runs the plugin’s new release.The gauge is now a button that opens a panel with what fills the window, and it no longer goes blank after a compaction.
What you may have to change:
- After a compaction or a Restore, the gauge shows an estimate with a
~prefix (~12K / 128K) instead of going blank until the next turn. A reading you used to wait for is now there at once; an estimate is never drawn as if it were the provider’s number.aparte-context-thresholdfires on the estimate as it does on a reported number. - The gauge’s markup changed. The reading (
.aparte-context__text) is inside a<button class="aparte-context__toggle">, and a hidden panel (.aparte-context__panel) follows it. The meter keeps itsrole="meter"and its label. CSS that targeted the gauge’s direct children needs a look. - A turn the model refused as over its window (
aparte-message-errorwhose error code isUSAGE_CONTEXT_EXCEEDED, for this gauge’s chat) turns itdangerwhatever the number says, until the next reply reports.
What is new, all additive:
- A disclosure: the button has
aria-expandedandaria-controls; click, Enter and Space open it, Escape and a click outside close it, and nothing opens on hover. The panel lists the system prompt, the tools, the summary of a compacted history, the messages, “other”, what is free, and the total against the window. The parts are estimates and say so; they are scaled down for display so they never add up to more than the total. - With a compactor registered (
setCompactor) whosepreview()returns something, the panel says “Compacting now would free ~N tokens” and offers Compact now (compact({ force: true, reason: 'manual' })). While it runs the element hasdata-compact="running", after a failuredata-compact="failed"and Retry in the panel. Both come from theaparte-compact-start,-doneand-errorevents of the gauge’s own chat. The gauge reads the config, and so the compactor, the system prompt, the tools and the window, from the chat it watches (target, else the chat above it), not from where it stands, so a gauge in a header reads a chat that carries its own config. aparte-compactdetail gainsauto(set by the deprecatedauto-compact, so the plugin can tell it from a button) andwindow(the window the gauge measured against). TheAparteContext.measuregetter returns the reading drawn.- Nine optional locale keys:
contextSystem,contextTools,contextSummary,contextMessages,contextOther,contextFree,contextTotal,contextReclaim({n}) andcontextCompactNow. Retry reusesretry. A locale without them falls back to the built-in English.
Why. The gauge drew the one number the last turn reported, so a compaction left it blank or wrong, and it said nothing about what the number was made of. Reading
measureContextover the chat’s own transcript fixes the first; the panel answers the second, and puts the one action that helps (Compact now) next to the number that asks for it.@aparte/core - After a compaction or a Restore, the gauge shows an estimate with a
-
37b82d9: Use
measureContext({ messages, config, windowOverride })to read how full the model’s window is: the same measure the context gauge draws and the compaction plugin decides on. Nothing for you to change.What is new, all additive:
measureContextreturns{ window?, used, estimated, parts: { system, tools, summary, messages }, other }.usedis what the next request will carry while nothing the model reads has changed since the last reply: the provider’s reported usage, except for a reply that thought, where it is the reported input plus the answer as it goes back on the wire (a provider counts its reasoning tokens as output, and the reasoning is not sent again). After a message, a compaction or a Restore it is an estimate (estimated: true) of the request that would be sent now: the system prompts, the tool definitions, the checkpoint’s summary and the messages after it, scaled by how far the heuristic was off on the last reply (its input tokens overcontextEstimate, kept between 0.5 and 3; 1 when the reply recorded none, and 1 when it called tools, because the estimate is of the first request and the usage of the last call, which also read the tool results).partsare the raw, unscaled estimates;otheris what they cannot attribute (a server loop’s own prompt and tools, template tokens, the heuristic’s error), never below zero.estimateTokens(text)andestimateWireTokens(messages). The second counts what the wire sends for those messages with the serialisation the client builds its history with: thinking is not counted, text and tool calls with their results are, a failed turn or one still in flight sends nothing.AparteContextMeasureandAparteContextMeasureInput(types).
Why. The plugin kept its own copy of the character heuristic and the gauge kept the number the last turn happened to report, so after a compaction the gauge was blank or stale and the plugin decided on figures the page never showed. They now read one function over one serialisation. The serialisation itself moved out of
AparteClientinto pure functions (wireMessagesand friends inclient/wire-history.ts); the requests it builds are the same as before.@aparte/core -
5aa437f: A message that carries a checkpoint (
contextSummary) now shows a separator above it: “12 messages summarised for the model · Show summary · Restore”. The messages above it stay on screen and in storage. Restore takes the checkpoints off and the model reads the original messages again.What you may have to change:
- A message written by an older compaction (
compaction: true) is no longer drawn as a muted bubble. The bubble draws the separator in its place (“Earlier messages were summarised”, with Show summary, and no count or Restore), and its own row does not paint. If your CSS targetedaparte-chat-bubble[data-kind="compaction"]for the old dashed notice, those rules no longer match anything visible; thedata-kindattribute is still written. - A custom bubble renders its own separator. The separator belongs to
<aparte-chat-bubble>; arenderBubble(React),bubbleslot (Vue, Svelte) orbubbleTemplate(Angular) that draws its own markup must readmessage.contextSummaryitself.
What is new, all additive:
<aparte-chat-bubble>.setContextSummary(summary | null, { legacy? }), andSyncableBubble.setContextSummary, whichpopulateBubbleFromMessagecalls for every message (with none, too, so a reused bubble loses its separator). The separator sits outside.aparte-message, inside the bubble, so a frame redraw or a custom shell keeps it.- States, driven by the compaction events: while a summary is written the separator says “Summarising the context…” (the shimmer recipe); if it fails it says “Couldn’t summarise the context” with Retry, which asks the chat’s registered compactor (
force, reason'retry') and falls back to dispatchingaparte-compactwhen none is registered. The viewport (and, under a wrapper, the framework host) readsaparte-compact-start,-doneand-error, acts only on those that name its own chat and a message of its own transcript (an untargeted event, which every chat receives, is checked against the message), and setsdata-checkpoint="pending"or"failed"on the bubble whose id isdetail.messageId. A-donefor that message clears it; a skipped-done, another message’s id and another chat’s events are ignored; the next-startand a conversation switch clear it. The newdata-checkpointattribute is read-only for you. - Restore asks “Send the original messages to the model again?” first. It then removes
contextSummaryfrom every message of the active path (through the chat’s ownupdateMessage, so a framework’s list and your storage see it) and dispatchesaparte-compact-donewithrestored: true. - Event details gain optional fields:
aparte-compact-start{ messageId },aparte-compact-done{ messageId, count, restored },aparte-compact-error{ messageId }.@aparte/plugin-compactiondispatches them. - The conversation is saved when a compaction finishes, once the message named by
messageIdcarries its checkpoint, and again after a Restore. A host that still writes acompaction: truemessage keeps the old behaviour. - Ten optional locale keys:
checkpointCount({n}),checkpointCountOne,checkpointLegacy,checkpointGroup,checkpointShow,checkpointHide,checkpointRestore,checkpointRestoreConfirm,checkpointPending,checkpointFailed. Retry and Cancel reuse the existingretryandcancel. A locale without them falls back to the built-in English. - A new sheet,
styles/components/checkpoint.css(aparte-checkpoint…classes), wearing the divider, ghost button and shimmer recipes.
Why. The compaction plugin used to replace the summarised turns with a summary message, so the turns left the screen. With a checkpoint over a kept history the reader sees everything they wrote and one line saying what the model no longer reads; Restore is the way back.
Detail for the curious.
--aparte-message-content-text-userand-assistantwere declared only by the old notice’s rule; they are now declared asinitialon the root, which reads as unset exactly as before. The viewport’srenderablelist gainedcontextSummaryandcompaction; the framework host’supdateMessagenow reaches the bubble of a message that is not the last forcontent,segments,attachments,usage,finishReason,contextSummaryandcompaction, where it used to stay in the list until the next full sync.Also in the separator. Confirming Restore removes the separator while its button holds the keyboard; the focus moves to the message the checkpoint sat on rather than falling to the page. A second failure with a different reason redraws the separator with the new reason. The summary panel wears the prose rules (
aparte-segment-content), so lists, quotes, links and code in a Markdown summary are drawn like the rest of the transcript.Deprecated: the
compactionSummaryTitlelocale key. Nothing reads it any more — the separator speaks through thecheckpoint*keys — and it stays until a later minor so a locale that sets it keeps compiling.@aparte/core - A message written by an older compaction (
-
eb3c0b1: A reply cut off at the model’s length limit now says so, and the
donestream event, the stored message andaparte-message-donecarry a new optionalfinishReason.Nothing to change in your code: a provider that reports no reason behaves as before. To see the note, a provider has to send
finishReason: 'length'on itsdoneevent (@aparte/provider-openai-compatand@aparte/provider-ai-sdkdo; so does a scripted turn of@aparte/provider-scenario).AparteFinishReasonis'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other'. It rides onAparteStreamEventMap.done, onAparteMessage.finishReason(persisted, so a reload still shows the note) and onAparteMessageDoneEventDetail.finishReason. It is the reason of the LAST model call of the turn: a tool turn that ends on a text answer reports that answer’s reason, never the earlier'tool-calls'.- The bubble draws a small muted line after the content of a completed assistant reply whose reason is
'length', and nothing for any other reason or while the reply streams. The text is the new locale keyresponseCutOff, in.aparte-finish-note(norole="alert": it is a fact about the answer, not an error).bubble.setFinishReason()andpopulateBubbleFromMessagefeed it, so wrapper-managed bubbles get it with the call they already make. - The server-loop path needed no change:
createAgentStreamRunnerreplays eachrun-doneline whole andcreateAgentHandlerwrites it whole; a test pins that.@aparte/core
-
eb3c0b1:
runStreamAgentnow emitsfinishReasononrun-done: the reason the LAST model call of the run ended, when its provider reported one.A step that reports no reason does not inherit the one before it: the value is reset at the top of every model call, so a final answer that says nothing is not described by the
'tool-calls'of the turn before.StreamChatEvent’sdoneaccepts the field (StreamFinishReason, the same union as core’sAparteFinishReason). A run whose providers report nothing emitsrun-doneexactly as before.@aparte/engine -
f0b38dd: A stream
errorevent can now carry acode, and the errorrunStreamAgentthrows for it carries the samecode. Nothing for you to change: an event without a code throws the plainErrorit always did.StreamChatEvent’serrorvariant gainscode?: string(core’s event map gains the typedcode?: AparteErrorCode; this is its mirror). The caller reads it from the thrown error: core’sAparteError.fromkeeps it when it is one ofAparteErrorCode’s values, which is how an overflowing prompt a provider reported mid-stream (USAGE_CONTEXT_EXCEEDED) becomes something the client can act on instead of a sentence to show.@aparte/engine -
1f62ccc: The
doneevent now carriesfinishReason, mapped from the AI SDK’s finish reason, so a reply the model stopped at its length limit shows as cut off.'stop','length','content-filter','tool-calls','error'and'other'pass through; the SDK’s'unknown'becomes'other'. A value the SDK adds later is left off rather than guessed.@aparte/provider-ai-sdk -
f0b38dd: An overflowing prompt reported by the AI SDK now carries
code: 'USAGE_CONTEXT_EXCEEDED'on theerrorevent, so core can compact and send the turn again. Nothing for you to change; any other error is the event it was.The bridge matches the vendor’s own sentence in the API error’s message, or in its
responseBodywhen the message is generic (“maximum context length”, “context size has been exceeded”,exceed_context_size_error, “exceeds the available context size”, “prompt is too long”,context_length_exceeded; any case), the same readings@aparte/provider-openai-compatmakes on a wire body.@aparte/provider-ai-sdk -
f0b38dd: The provider now tells an overflowing prompt from a bad request: a 400 or 413 whose body says the prompt did not fit is
USAGE_CONTEXT_EXCEEDED, and so is an error the server streams after its 200. Nothing for you to change; with@aparte/plugin-compaction, core compacts and sends the turn again instead of showing the error.What is new, all additive:
provider.classifyError(status, body)(theAparteFormatAdaptermethod core’s direct transport andcreateAparteChatHandlercall):USAGE_CONTEXT_EXCEEDEDfor a 400 or 413 whoseerrorsayscontext_length_exceeded, or whose message, type or code matches “maximum context length”, “context length”, “context size has been exceeded”, “exceed_context_size_error”, “exceeds the available context size” or “prompt is too long” (any case). Every other status and body is left to the status.- The mid-stream
errorevent the provider emits for the same payloads carriescode: 'USAGE_CONTEXT_EXCEEDED'; so does the errorparseTextthrows for a 200 body that is an overflow (LM Studio). Any other error carries no code, as before.
Needs
@aparte/corewith the same release to be acted on; older cores ignore the code and show the error as they did.@aparte/provider-openai-compat -
45ed986: Errors a server sends mid-stream now surface as errors, a reply’s finish reason is reported, and the context window of LM Studio, Ollama, llama.cpp and vLLM models is read.
Three changes, none of which asks you to touch your code:
- Stream errors. A server that answers
200and then streamsdata: {"error": {...}}(LM Studio on a context overflow, OpenAI and vLLM mid-stream) used to end the turn on an emptydone: an empty reply marked completed. The line is now anerrorevent carrying the server’s message, and the stream ends there without adone. The non-streaming path throws the same message for a200body that haserrorand nochoices, where it returned''. - Finish reason. The last non-null
finish_reasontravels on everydoneasfinishReason:stop,length,tool-calls(tool_callsandfunction_call),content-filter, orotherfor anything else. A server that never says leaves it off. - Context window.
fetchModelsread onlycontext_length, so a local model had nocontextWindow: the gauge never showed and compaction never triggered while the server truncated silently. It now readscontext_length,max_model_len,context_windowormax_context_lengthfrom the/modelsentry, and the newserveroption ('lmstudio' | 'ollama' | 'llamacpp' | 'vllm') makes one extra request at the endpoint’s origin for the window the server actually serves: LM Studio’s/api/v1/models(a loaded model’scontext_length), Ollama’s/api/ps, llama.cpp’s/props. A failing probe leaves the models without a window, with one warning. AcontextWindowyou declare on a model inmodelswins over anything read.
The
LMSTUDIOandOLLAMApresets now setserver, and two presets are new:LLAMACPP(http://localhost:8080/v1) andVLLM(http://localhost:8000/v1).An unloaded LM Studio model, and an Ollama model not currently loaded, get no window rather than the trained maximum: that number is not what the server will run with, and a gauge drawn against it is worse than none.
@aparte/provider-openai-compat - Stream errors. A server that answers
-
1f62ccc: A scripted turn can end with
{ finishReason: 'length' }to setfinishReasonon itsdoneevent, so a demo can show the “cut off” note.A turn that declares none reports none, as before.
@aparte/provider-scenario -
d68ace6: The summary request is now bounded: streamed, capped by
maxTokens, refused without a known window, trimmed to fit, and a summary that is cut off, failed or no shorter than what it replaces is refused.What you may have to change:
- A model with no
contextWindowno longer compacts by default.setupCompaction()reports{ skipped: true, reason: 'no-window' }(newCompactionSkipReason) instead of sending everything but the last four messages in one request. Declare the window on the model, or size the request yourself: passingkeepWithoutWindow(the opt-in; it keeps today’s behaviour), your ownselectoror your ownsummarizeis enough. - A
summarizeof yours now receivesstream: trueandmaxTokenson the request, where it used to receivestream: falseand no limit. If it forwards the request to a model as is, it now gets a stream back; if it expects a string, setstream: falseon the request you forward.
What changed, and why. A live test against a local model with a 4096-token window showed the summary request was bounded by nothing:
- The request is streamed and drained strictly. An
errorevent fails the compaction with its message instead of applying the text received before it, and adonewhosefinishReasonis'length'fails with “The summary was cut off at the model’s length limit”. A plain string answer is still the whole summary. request.maxTokenskeeps the answer inside the window: a quarter of the window, at mostreservedGeneration(2000, the room every request keeps for a reply) and never under the room the window walk holds back for the summary;reservedGenerationwhen there is no window. It is deliberately looser than that reservation (about 118 tokens on a 4096-token model), which a reasoning model spends thinking before it writes a word.- With a known window and the default summariser, the instruction plus the transcript of the dropped turns must fit the window less
maxTokens, the reserved thinking and the safety margin. When they do not, the oldest turns that fit are summarised and the rest stay verbatim for the next compaction. If not even the oldest turn fits, the compaction fails with an error instead of sending it. - A summary whose estimate is not smaller than the turns it replaces fails with “The summary is no shorter than the turns it replaces”, and the transcript is left untouched. This one applies to a host’s
summarizetoo.@aparte/plugin-compaction
- A model with no
-
62c70d4: Compaction no longer deletes messages, and it is automatic once set up. A compaction now writes a checkpoint (
contextSummary) on the first message it keeps, and the history the client sends resumes from it; the messages above stay on the screen and in storage.setupCompaction()also compacts by itself: after a turn that ends at 80 % of the window, before a send whose request would not fit, and when the model refuses a request as over its window (the turn is sent once more). Passauto: falseto keep it manual only (the gauge’s Compact now, a separator’s Retry,compact(),aparte-compact): none of the three automatic attempts is made, the overflow retry included.What you may have to change:
- A page that only wanted the manual compaction passes
auto: false.autodefaults totrue, and is forced tofalsewithlisten: false.auto: falsemeans manual only:compact()answers an'auto'or'overflow'request with a skip ('nothing-to-drop'), so the model’s overflow error stands as it did before the plugin. It acts only when a summariser can run (a provider registered for the current model, or yoursummarize), and only when a window is known: the model’s, or the newcontextWindowoption. - A
CompactionTargetof your own needsupdateMessage(id, updates). It is the one write a compaction makes. Without it the compaction fails with an error that says so, before the model is paid; there is no fallback that rewrites the transcript.clearAllandappendMessageare no longer called (they stay optional on the type).<aparte-chat-viewport>already hasupdateMessage. DEFAULT_COMPACTION_PROMPThas a new value (the name is unchanged). It asks for fixed sections (the goal and constraints, the decisions and why, the open tasks, the facts and tool results still needed, the next step) in the language of the conversation, and keeps the clause that stops the model from answering the transcript. If you pass your ownprompt, nothing changes for you.summaryin the outcome and inaparte-compact-doneis what is stored: the model’s summary, then, on the first compaction of a conversation, the first user message verbatim (clipped at 1 500 characters) under a fixed English line. The conversation’s first request survives every compaction.- A
selectoryou pass receives the model’s view (the messages from the latest checkpoint on, less the turn being prepared), and the cut is moved so the first message kept is a user message: it may keep more than your selector asked for, or skip with'nothing-to-drop'when no user message can carry the checkpoint. - The “notice” message is gone from new compactions: no
compaction: truemessage is written, and thecompactionSummaryTitlelocale key is no longer read (old stored conversations keep the title inside their summary message, and are drawn as a separator).
What is new:
- Options
auto,threshold(default0.8) andcontextWindow(a number, or a function, for a server that does not declare its window).thresholdis also what a compaction aims below: with the default selector it leaves the history the model reads at aboutthreshold − 0.2of the window, less the fixed costs (system prompt, tools, the reply’s room, the summary’s slot), so one compaction buys several turns. That holds for a person’s request and an overflow retry too, and aselectorof your own is left alone. Thewindowa context gauge sends with itsaparte-compactrequest is read as a last resort for one more release. - The controller is registered as the config’s compactor (
config.setCompactor) and unregistered bydispose(), so the gauge’s Compact now, a separator’s Retry, the client’s pre-flight and its overflow retry all call it.compact()takes the compactor request ({ target, targetId, force, exclude, reason }) as well as a chat id or nothing, resolves the chat from the element it is given (a chat in a shadow root is found), and a scoped setup refuses a chat outside its scope. Newsettled(),beforeSend()andpreview(). - A second compaction updates the first: the request carries “Summary so far:” and only the messages since, and asks for an updated summary. The no-gain refusal compares against what is replaced: the earlier summary plus the newly summarised messages.
- Two failures in a row for a chat stop the automatic attempts until something succeeds, a person asks (
reason: 'manual'or'retry', or a request withoutauto), or the conversation is switched; with automatic compaction on, an overflow is still compacted and resent. After a compaction that still leaves the context at the threshold, the after-turn trigger waits until a turn ends below it (a request that would not fit is still compacted before it goes out), and never the same range twice. A Restore suspends the automatic attempts for that chat until a person asks again. createCompactionSelectortakes an optionalestimate(message). The selectorsetupCompactionbuilds uses core’sestimateWireTokens, so it prices what the wire sends (a reply’s hidden thinking costs nothing). The selector it returns takes an optional second argument,CompactionLimits(scale,reserve,landAt,ceiling), which the default selector is called with on each compaction: the heuristic’s undercount as the context measure calibrates it (the selection, the summary request’s size and the pre-send comparison all use it), room for the turn being prepared, the share of the window to land at, and, after an overflow, a ceiling below what failed.- Chats compact independently: a summary being written for one chat no longer skips another’s pre-send compaction or overflow retry.
settled(),beforeSend()and an overflow wait for their own chat’s summary only;runningandabort()cover every chat of the setup. - A Restore cancels a summary still being written for that chat, and a summary is refused when the checkpoint it updates was taken off meanwhile, so a Restore is not undone seconds later.
- The compactor request’s
signal(andbeforeSend’s) cancels the chat’s summary: a Stop, or a programmaticstop(), no longer pays for a summary whose turn is gone, whether or not the setup listens onwindow. It is not counted as a failure. - The summary request alternates roles: adjacent turns of one role are merged and the ask joins a trailing user turn, so a strict chat template (Mistral, Gemma on llama.cpp or LM Studio) no longer refuses it. An inline
<think>…</think>block (or one left open, or a closing tag alone) is stripped from the summary before it is stored. CompactionOutcomeis core’sAparteCompactorOutcome;CompactionControllerextends core’sAparteCompactor. Both gainmessageIdandcount. Theaparte-compact-start,-doneand-errorevents carry themessageIdthe checkpoint sits on.
The checkpoint is the plugin’s half of core’s
contextSummary,historyFromCheckpointandsetCompactor; the separator that draws it, and the history that resumes from it, are core’s.@aparte/plugin-compaction - A page that only wanted the manual compaction passes
-
37b82d9: Adds the French text for the nine keys of the context gauge’s new panel, such as “Prompt système”, “Libre” and “Compacter maintenant”. Nothing to change in your code.
The keys are
contextSystem,contextTools,contextSummary,contextMessages,contextOther,contextFree,contextTotal,contextReclaimandcontextCompactNow. The panel is what the gauge opens: what fills the model’s window, and a button to compact the conversation now.@aparte/locale-fr -
5aa437f: Adds the French text for the ten keys of core’s new context separator, such as “12 messages résumés pour le modèle”, “Afficher le résumé” and “Restaurer”. Nothing to change in your code.
The keys are
checkpointCount,checkpointCountOne,checkpointLegacy,checkpointGroup,checkpointShow,checkpointHide,checkpointRestore,checkpointRestoreConfirm,checkpointPendingandcheckpointFailed. The separator is the line a chat draws above the first message the model still reads verbatim, once the messages before it were summarised.@aparte/locale-fr -
eb3c0b1: Adds the French text for core’s new
responseCutOffkey: “Coupée à la limite de longueur du modèle.”It is the note under a reply the model stopped at its length limit.
@aparte/locale-fr
Patch Changes
-
7a8eac4: Fix three problems after a compaction or a new chat: the model no longer receives two
usermessages in a row, the conversation title is no longer taken from the summary, and the context gauge no longer keeps the previous conversation’s reading.Nothing to change in your code.
- Two user turns in a row. After a compaction the summary is a
usermessage and the kept turns usually start with one; after a failed turn its user message stays while its error reply is not sent. Either gave the model adjacentusermessages, which strict chat templates (Gemma, Mistral, llama.cpp with--jinja, vLLM) reject, and every following turn failed. Send, retry, edit and the resume of a paused turn now merge adjacentusermessages into one on the final array (texts joined by a blank line, image parts kept). System, assistant and tool messages are never merged. - The title.
AparteConversationManagertook the title from the firstusermessage, which after a compaction is the summary notice (“Conversation summary The user requested…”). The three places that look for it skip acompaction: truemessage. - The gauge.
<aparte-context>listened to turns and compactions only, so after “New chat” the empty conversation still showed the last reading. It now clears onaparte-reset-doneand, for a stored conversation, restores the last reply’susageonaparte-conversation-loaded. A restored reading never dispatchesaparte-compact: auto-compact is now asked only at the end of a turn, so a gauge already indangerno longer asks again on a window or config change.@aparte/core
- Two user turns in a row. After a compaction the summary is a
-
b140b2b: Under a wrapper, a message the parent replaces with a new object is drawn again, whether or not it is the last one. A list you hand in (a controlled
messagesprop, av-model,bind:messages, an Angular input) that changes an earlier message, for example a store-backed compaction target writing a checkpoint onto the first kept message, now reaches that message’s bubble.Nothing to change in your code. An update that keeps the same message object is still skipped, so a list you rebuild with the same objects costs nothing more than before; a list that copies every message on every render makes every bubble redraw on every render.
Why.
AparteChatHost.syncBubblesdrew every message once and then only the last one on each render, so a change written through the parent’s list to an earlier message stayed in the list and never reached the screen. It was invisible with the host’s ownupdateMessage, which pokes the bubble itself, and it left the separator of a compaction missing on every framework-rendered transcript whose list was the app’s. The host now remembers the object each bubble was drawn from and compares it by reference, which an immutable update always changes. The same one change serves React, Vue, Svelte and Angular, since they share the host.@aparte/core -
8280572: A model listed without a context window gets it after its first reply: the client reads the provider’s model list again, once per model, so an LM Studio or Ollama model loaded on demand gets a gauge and compaction.
A local server that loads a model on its first request has no loaded window to report when the list is fetched at start-up, so the model came without one and the gauge and the compaction had nothing to measure against for the whole conversation. After a reply the model is loaded and the provider’s probe finds the window.
refreshProviderModels()now announces a change (aparte-config-change) when the current model’s window changes, and only then; and an empty answer — what a provider returns when its fetch fails — keeps the list already held instead of wiping the selected model’s capabilities.@aparte/core -
37b82d9:
estimateTokensnow comes from@aparte/core. The plugin re-exports it under the same name, so nothing changes for you.The gauge and the plugin used to keep their own copy of the character heuristic; core’s
measureContextreads one, and the plugin’s budget now reads that one too. The function and its result are the same:ceil(characters / 3.8).@aparte/plugin-compaction -
5c7407a: The published types keep
nullandundefinedagain:[conversationId]="id()"with astring | nullsignal now compiles understrictTemplates.Code written against the old, wrong types may need a null check: on the conversation service,
activeConversation()isAparteConversation | nullandactiveId()isstring | null; on the chat,bubbleDirective()can beundefined. The runtime always returned those.The package was built with ng-packagr’s built-in tsconfig, which sets no
strict, so the declarations were emitted withoutstrictNullChecksand every inferrednull/undefinedvanished:conversationId,typingText,config,containerClass,containerStyle, theconfigof both tool directives. The build now passes-c tsconfig.lib.json, which is ng-packagr’s own options plusstrict: the emitted JavaScript is byte for byte the same, only the declarations change. The Angular SSR fixture binds those inputs to nullable signals understrictTemplates, so a build that drops them again fails CI. Reported in #124.@aparte/angular
Version-only bumps (no changes of their own): @aparte/runner-ag-ui, @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/react, @aparte/svelte, @aparte/vue, @aparte/docs-mcp.