Utilities — core API
Generated reference for @aparte/core. Small helpers: escaping, ids, copying, attachments from files, segment timing, and what the framework wrappers share.
On this page
Functions
Section titled “Functions”registerAllComponents()
Section titled “registerAllComponents()”function registerAllComponents(): void;Defined in: packages/core/src/index.ts:471
Make sure every element core defines is in the registry, and say which are not.
Importing @aparte/core already registers all of them. Call this when a bundler with
aggressive side-effect elimination, or a dynamic import(), might have dropped the
modules — the call itself is what keeps them, because it reaches APARTE_ELEMENTS.
Idempotent, and safe to call more than once: it defines nothing, it reports.
Returns
Section titled “Returns”void
installDialogTriggersOnce()
Section titled “installDialogTriggersOnce()”function installDialogTriggersOnce(doc?): void;Defined in: packages/core/src/interop/dialog-triggers.ts:92
Wire data-aparte-dialog-open / -close and the backdrop click on this document.
Called once by @aparte/core at import; safe to call again.
Parameters
Section titled “Parameters”| Parameter | Type | Default value |
|---|---|---|
doc | Document | document |
Returns
Section titled “Returns”void
applyElementProps()
Section titled “applyElementProps()”function applyElementProps(
el,
props,
transformValue?): void;Defined in: packages/core/src/interop/element-props.ts:119
Apply props to an aparté custom element. aparté elements are
attribute-driven (observedAttributes): assigning a property is either a
silent no-op (nothing observes it) or throws outright on a getter-only
accessor — <aparte-composer>’s placeholder/disabled are exactly that.
So primitives go through setAttribute; only values an attribute cannot carry
(objects, functions) are handed over as properties. Keys starting with --
become CSS variables; on… function values are ignored (event forwarding is
the wrapper’s job).
transformValue is applied to object/function values before they’re set as
properties — Vue passes toRaw to unwrap its reactive proxy (a deep proxy
breaks Maps/class internals on a plain custom element); it defaults to identity.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
el | HTMLElement |
props | Record<string, unknown> |
transformValue | (value) => unknown |
Returns
Section titled “Returns”void
copyText()
Section titled “copyText()”function copyText(text): Promise<void>;Defined in: packages/core/src/utils/copy-text.ts:23
Copy text to the clipboard, in every context the library actually runs in.
navigator.clipboard is secure-context only — the same wall uuid.ts documents
for crypto.randomUUID. On http://192.168.1.x, the archetypal deployment for this
library’s own audience, the property is undefined, so
navigator.clipboard.writeText(text) throws a TypeError synchronously — before any
.catch() can see it. The three copy buttons core renders (the bubble’s action bar, a
code block, the artifact card) each had a .catch() for a REJECTED write and nothing
for a missing API: on plain http they threw in the click handler and gave no feedback
at all, not even a failed one.
The fallback is document.execCommand('copy') — deprecated, but every engine still
honours it from a user gesture and it needs no secure context. The text goes into an
off-screen textarea, is selected, copied, and the textarea is removed; focus is handed
back to where it was, so a keyboard user does not lose their place on the button. It is
the path clipboard.js and copy-to-clipboard take, for the same reason.
Rejects when neither path could copy, so a caller’s “copied” confirmation can stay
honest. pnpm check:secure-context refuses a bare navigator.clipboard anywhere but
here, so the sweep cannot come undone one call site at a time.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
text | string |
Returns
Section titled “Returns”Promise<void>
cssEscape()
Section titled “cssEscape()”function cssEscape(value): string;Defined in: packages/core/src/utils/css-escape.ts:19
Escape a value for safe interpolation into a DOUBLE-QUOTED CSS attribute
selector: [attr="${cssEscape(value)}"]. Inside a quoted CSS string only "
(which would end the string) and \ (the escape char) are special, so escaping
those two is sufficient AND correct here — unlike CSS.escape, which targets the
UNquoted identifier context and over-escapes inside quotes (breaking the match),
and which also needs the CSS global (absent in SSR / some test runtimes).
Segment/message ids are random UUIDs in the default flow, so this is
defense-in-depth: a hostile, stream-supplied id containing " (e.g. a tool-call
id) can no longer break out of the selector into a SyntaxError or a
selector-list that mis-targets another element.
A raw newline (or CR / form feed) is not allowed inside a CSS string either — it
ends the string and the selector throws the same SyntaxError. Escaped as its
code point followed by a space, the form the CSS Syntax spec gives for exactly
this (\a ), so an id a provider minted with a line break still matches.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
value | string |
Returns
Section titled “Returns”string
escapeHtml()
Section titled “escapeHtml()”function escapeHtml(value): string;Defined in: packages/core/src/utils/escape.ts:25
HTML escaping — one implementation, for every interpolation core makes.
There used to be SEVEN copies of this across the package (_escapeHtml in the
bubble and in the config, _escapeAttr in the chat shell, the composer action
and the composer input, plus escapeHtml and escapeAttr in the segment
renderers). They had already drifted: the chat shell’s copy escaped only &,
" and <, leaving ' and > through, so it was strictly weaker than its
six siblings while looking identical at the call site. That is the failure mode
a duplicated security helper always ends in — one of them stops matching, and
nothing tells you which.
Escapes all five characters that matter, which makes the result safe in BOTH text position and quoted-attribute position (single or double quotes). Callers that read better saying “attribute” use escapeAttr, which is this same function under a second name — an alias, deliberately not a second body.
PUBLIC. Both names are exported from the barrel and documented in the
customization guide, because a render hook that returns a string needs them and
telling an author to “escape it yourself” without naming the function is how a
capability becomes invisible. An earlier version of this comment claimed the
opposite (“not exported from the package barrel… not a public API”) while
index.ts exported both — the export was real, the comment was not.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
value | string |
Returns
Section titled “Returns”string
filesToAttachments()
Section titled “filesToAttachments()”function filesToAttachments(files): AparteAttachment[];Defined in: packages/core/src/utils/files-to-attachments.ts:35
Turn the File[] an aparte-send event carries into the
AparteAttachment[] a message renders.
The composer hands you raw Files; a bubble needs an id, a MIME type and a
url to show a chip or a thumbnail. The ConversationController (used by the
framework wrappers) does this internally — this is the same conversion,
exported so a raw-core consumer driving appendMessage() itself doesn’t have
to hand-roll object URLs:
chat.addEventListener('aparte-send', (e) => {
const { content, files } = e.detail;
chat.viewport?.appendMessage({
id: uuid(), role: 'user', content, timestamp: Date.now(),
...(files?.length ? { attachments: filesToAttachments(files) } : {}),
});
});The url comes from URL.createObjectURL, so it lives as long as the
document; the raw File rides along on blob for storage adapters that
persist attachments and rebuild the url on reload.
That lifetime is deliberate for a rendered attachment — but it is also a leak
once the attachment is gone: a long session that sends many files retains every
File for the life of the page. Call revokeAttachmentUrls when you know
they are no longer rendered; core does it on clearAll().
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
files | readonly File[] |
Returns
Section titled “Returns”revokeAttachmentUrls()
Section titled “revokeAttachmentUrls()”function revokeAttachmentUrls(attachments): void;Defined in: packages/core/src/utils/files-to-attachments.ts:57
Release the object URLs of attachments that are no longer on screen.
Nothing revoked them, so every File a session sent stayed reachable until the
page went away. Only the caller knows when an attachment stops being rendered —
a persisted conversation may re-render one much later — so this is a function
rather than something filesToAttachments could schedule.
Safe to call twice: revoking an already-revoked or foreign URL is a no-op, and a
blob is left in place so a storage adapter can still rebuild the url.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
attachments | readonly AparteAttachment[] | undefined |
Returns
Section titled “Returns”void
isAwaitingReply()
Section titled “isAwaitingReply()”function isAwaitingReply(message): boolean;Defined in: packages/core/src/utils/is-awaiting-reply.ts:22
Is this message waiting for a reply — i.e. should the bubble render as
in-progress (waiting indicator, aria-busy, no action bar) rather than as a
finished answer?
Two clauses:
- Stated:
statusis'streaming'or'pending'. This is exactly the condition every call site used before this helper existed, role included, so no existing behaviour shifts. - Inferred: an assistant message with no
statusat all and nothing in it — nocontent, nosegments. That is the empty shell a token stream is about to fill (appendMessage({ role: 'assistant', content: '' })from a hand-rolled loop). It used to render as a completed reply, action bar and all, on a message that had never said anything.
Only silence is interpreted. An explicit status — including 'completed' on an
empty message — is the application talking, and is believed.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
message | Pick<AparteMessage, "role" | "status" | "content" | "segments"> |
Returns
Section titled “Returns”boolean
segmentTiming()
Section titled “segmentTiming()”function segmentTiming(segment): AparteSegmentTiming | undefined;Defined in: packages/core/src/utils/segments.ts:159
Read core’s own measurements off a segment.
A function rather than a property access because it is read from six places, and
segment.meta?.aparte spelled six times is six chances to spell it differently —
which is exactly how this repo’s recurring bug starts.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
segment | Pick<AparteSegment, "meta"> |
Returns
Section titled “Returns”AparteSegmentTiming | undefined
isSegmentSettled()
Section titled “isSegmentSettled()”function isSegmentSettled(segment): boolean;Defined in: packages/core/src/utils/segments.ts:219
Has this segment settled? The trigger for endedAt.
Exported on purpose: a consumer rendering its own “thought for 8 s” line needs the same predicate, and a rule buried inside a component method is a rule that gets re-derived slightly differently somewhere else.
A segment with no streaming flag at all is NOT settled. That is deliberate: a
reply that arrived whole never had a span to measure, and stamping an endedAt
equal to its startedAt would dress a zero up as a measurement.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
segment | AparteSegment |
Returns
Section titled “Returns”boolean
segmentDuration()
Section titled “segmentDuration()”function segmentDuration(segment): number | undefined;Defined in: packages/core/src/utils/segments.ts:326
How long this segment took, or undefined when that is not knowable yet.
The five identity/measurement fields are optional because they describe a LIFECYCLE: a segment built by hand or freshly emitted by the parser has not been inserted yet, so it has no start, and an open segment has no end. Making them required would either force every creation site to stamp — six of them, which is the forgotten-sibling shape this seam exists to prevent — or force a hand-written literal to invent values the owner then refuses to overwrite.
The cost of that optionality lands on the reader, and this is what removes it. The guard it replaces was written three times in this repo before it earned a function:
if (!isSegmentSettled(seg) || !seg.startedAt || !seg.endedAt) return;— three conditions to get right, and the last two are wrong at epoch 0, where a valid timestamp is falsy. Nobody streams in 1970, but the test suite sets the clock to 0 routinely, so the trap was already in the repo waiting for its first victim.
It deliberately does NOT ask whether the segment is finished: during a turn a growing duration is exactly what a “thinking…” counter shows. Pair it with isSegmentSettled when you need the final number.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
segment | AparteSegment |
Returns
Section titled “Returns”number | undefined
uuid()
Section titled “uuid()”function uuid(): string;Defined in: packages/core/src/utils/uuid.ts:25
A unique id, in every context the library actually runs in.
crypto.randomUUID is secure-context only. On http:// — which includes
http://192.168.1.x, the archetypal deployment for this library’s own audience
(bring-your-own-key, a local model on the LAN box) — it does not exist, and
calling it throws TypeError: crypto.randomUUID is not a function.
That was not a theoretical edge. A cold audit found 27 call sites across ten
files and exactly TWO of them guarded, and the first thing to break was the
stream parser: _generateId() runs from _createTextSegment, so
parser.parse('hello') threw on the very first token of the very first reply.
The two guarded sites — conversation-controller.ts and
files-to-attachments.ts — prove the hazard was known and the sweep was never
finished. This is that sweep, in one place.
The fallback is NOT cryptographic and does not need to be: these ids address
DOM nodes and map keys inside one page. Where a real UUID matters (a persisted
conversation id a consumer may treat as stable), crypto.randomUUID is still
used first, so a secure context gets the strong value.
pnpm check:secure-context refuses a bare crypto.randomUUID anywhere but
here, so the sweep cannot come undone one call site at a time.
Returns
Section titled “Returns”string
Variables
Section titled “Variables”APARTE_DEFAULT_UI_EVENTS
Section titled “APARTE_DEFAULT_UI_EVENTS”const APARTE_DEFAULT_UI_EVENTS: readonly ["aparte-send", "aparte-cancel", "aparte-composer-change", "aparte-composer-submit", "aparte-action-click", "aparte-attachment-preview", "aparte-action", "aparte-retry", "aparte-edit", "aparte-feedback", "aparte-message-info", "aparte-branch-navigate", "aparte-link-click", "aparte-segment-update", "aparte-reset-done", "aparte-path-changed", "aparte-conversation-select", "aparte-conversation-delete", "aparte-conversation-archive", "aparte-conversation-unarchive", "aparte-conversation-rename", "aparte-conversation-pin", "aparte-conversation-unpin", "aparte-conversation-loaded", "aparte-select-change", "aparte-select-open", "aparte-select-close", "aparte-optgroup-toggle", "aparte-suggestion", "aparte-context-threshold", "aparte-scroll-rail-jump", "aparte-sidebar-toggle", "aparte-split-resize", "aparte-message-start", "aparte-message-done", "aparte-message-error", "aparte-message-aborted", "aparte-message-paused", "aparte-tool-approval-request"];Defined in: packages/core/src/interop/element-props.ts:41
Every custom event an aparté element dispatches on ITSELF — the set AparteUi
forwards through its single event output, in all four wrappers.
It said “verified against core” while carrying seven of twenty-five, and the gap was
not academic: aparte-model-change was missing, which is the event the ONE example
in the wrappers’ own docs exists to receive (<aparte-ui name="aparte-model-selector" …>). The documented usage could not hear the thing it was documenting. Re-derived
from the generated manifest, which now records every element’s events because their
docblocks were reattached.
A PLUGIN’s events are not here either, and aparte-model-change was until this line
was written. The reason is the same one that keeps plugin tags out of the element
registry: this default is core’s, and a third-party plugin’s author cannot add to it,
so listing ours would privilege our packages over theirs. Pass the names you need —
events: ['aparte-model-change'] — which is one line for everyone equally.
Three events are deliberately NOT here for a different reason: aparte-abort,
aparte-compact and aparte-config-change go out through window.dispatchEvent —
they concern the whole page, not one subtree — so an element-level listener can never
receive them and listing them would promise a forward that cannot happen.
aparte-message-aborted used to be named in that sentence, and it was wrong.
dispatchLifecycleEvent dispatches it — with the other four lifecycle events — on the
HOST ELEMENT, bubbling and composed (client/lifecycle-events.ts), and the composer’s
window dispatch is a SECOND, page-wide broadcast, not the only one. So the proxy
could have forwarded it all along, and a wrapper consumer watching a turn end had to
reach past <AparteUi> to window for no reason. The five lifecycle events are here
now; the sentence above lists only what a window dispatch is the sole path for.
scripts/check-event-map.mjs now asserts this list against every non-window
dispatch in core, so “verified against core” is a check rather than a claim — which
is what the first version of this docblock said while carrying seven of twenty-five.
escapeAttr
Section titled “escapeAttr”const escapeAttr: (value) => string = escapeHtml;Defined in: packages/core/src/utils/escape.ts:58
The same escaping as escapeHtml, named for the position it guards.
An attribute value needs exactly the same five characters escaped as a text node — quotes to keep the value from ending early, angle brackets so a broken value cannot start a tag, and the ampersand so none of it can be smuggled back in as an entity. Two names, one body.
REQUIRES A QUOTED ATTRIBUTE. title="${escapeAttr(v)}" and title='…' are
both safe; bare title=${escapeAttr(v)} is NOT, because a space, tab, newline
or backtick in the value ends an unquoted attribute and starts the next one —
and none of those five escaped characters is one of them. An audit raised this
as a latent gap (no unquoted interpolation exists in the library today).
The fix is to quote the attribute, not to escape harder: encoding spaces would
make every legitimate title="Hello world" read Hello world. So
pnpm check:attr-escaping now rejects an unquoted attribute interpolation
outright rather than trusting this function to cover a position it cannot.
HTML escaping — one implementation, for every interpolation core makes.
There used to be SEVEN copies of this across the package (_escapeHtml in the
bubble and in the config, _escapeAttr in the chat shell, the composer action
and the composer input, plus escapeHtml and escapeAttr in the segment
renderers). They had already drifted: the chat shell’s copy escaped only &,
" and <, leaving ' and > through, so it was strictly weaker than its
six siblings while looking identical at the call site. That is the failure mode
a duplicated security helper always ends in — one of them stops matching, and
nothing tells you which.
Escapes all five characters that matter, which makes the result safe in BOTH text position and quoted-attribute position (single or double quotes). Callers that read better saying “attribute” use escapeAttr, which is this same function under a second name — an alias, deliberately not a second body.
PUBLIC. Both names are exported from the barrel and documented in the
customization guide, because a render hook that returns a string needs them and
telling an author to “escape it yourself” without naming the function is how a
capability becomes invisible. An earlier version of this comment claimed the
opposite (“not exported from the package barrel… not a public API”) while
index.ts exported both — the export was real, the comment was not.
Parameters
Section titled “Parameters”| Parameter | Type |
|---|---|
value | string |
Returns
Section titled “Returns”string
Type Aliases
Section titled “Type Aliases”AparteUiEventName
Section titled “AparteUiEventName”type AparteUiEventName = typeof APARTE_DEFAULT_UI_EVENTS[number];Defined in: packages/core/src/interop/element-props.ts:103
Every event name aparté dispatches ON an element, as a type.
as const rather than readonly string[] because a TYPE needs the literals: the
Svelte wrapper derives its on: surface from HTMLElementEventMap, which
deliberately omits the five detail-less events (a map entry would type e.detail as
null and gain nothing). That omission was harmless while it only governed
addEventListener; once the wrapper DECLARED the tags it removed
SvelteHTMLElements’ catch-all index signature, and on:aparte-cancel — the stop
button — stopped type-checking. This list already knows those five, so it is the
honest source for “every event you can listen for”.