Skip to content

v0.31.0

Choose a tag to compare

@angel-ip angel-ip released this 06 Oct 14:31
· 433 commits to main since this release
3872bb9

v0.31.0

The dashboard, block A of 0.31. Six breaking changes in @kanzo-tech/ui/analytics, and what to
edit for each:

  • parseDashboards(json) replaces migrateDashboards, and parseDashboard(json) reads one
    spec. Both check what you stored against the spec's schema and either return it or throw
    not a dashboard spec at <path>: …. null or undefined reads as no dashboards. Nothing is
    migrated: a spec saved by an earlier release is refused, so catch the error and delete what you
    stored.
  • DashboardSpec and Dashboards have no version field. Stop writing one.
  • onChange is called with undefined on "Reset to automatic". Delete what you stored for that
    relation; the dashboard follows the relation's statistics again. The type is now
    (spec: DashboardSpec | undefined) => void.
  • TileEditor is a popover beside the tile it edits, and it draws no tile. tile is now the
    draft (a Tile, no longer Tile | null) and onChange receives every change to it: draw that
    draft in the tile's own view, and pass that view's element as anchor: () => HTMLElement | null.
    table and config are gone. Mount it only while a tile is being edited. Dashboard already does
    all of this.
  • A figure (StatTile) has no span. The figures are drawn as one band above the grid, sharing
    its row equally, so a figure never stands as tall as the chart beside it; charts and tables keep
    their span in the grid below. Drop span from every kind: "stat" tile you write; a stored spec
    that still has one is refused by parseDashboard.
  • column is no longer exported. Use numbers(data, field).

New:

  • Recommendation.rule names the rule that proposed a chart, and an automatic tile records it as
    origin: { rule }. The card's "why" is read from it, and it is dropped once somebody changes what
    the tile reads, or renames it.
  • A read-only Dashboard (no onChange) no longer downloads the editor. It is loaded the first time
    somebody edits.
  • Opening a dialog or a popover no longer redraws every chart on the page.
  • A popover whose body is taller than the room it has scrolls between its header and footer.
  • Quiet controls (the tile's edit button, the filter chips' chevron, the relation picker, a table's
    idle sort icon) use the muted text colour instead of opacity, so they follow the theme.

valibot is now a dependency of @kanzo-tech/ui. Your package manager installs it.

@kanzo-tech/ui

A preference is a part: <Pref name>, usePref and SectionProvider. Breaking.

  • PreferencesSections loses only and specimens. Write the parts instead:
    <PreferencesSections namespace="theme" only={["density"]} /> becomes <Pref name="theme.density" />,
    and specimens={{ "graph.look": … }} moves to the section's owner,
    <SectionProvider namespace="graph" specimens={{ look: … }}>.
  • usePref(name) answers { value, setValue, options, offered, shown } for a control of your own.
  • SectionProvider lets a section's owner answer the lists its choices name and the pictures its
    options wear, for the subtree beneath it. KanzoThemeProvider is the core's; useKanzoTheme() no
    longer returns sources.
  • A choice whose options come from a source is drawn as a Select; every other choice is a row of
    radio cards, a specimen above the name.
  • Names are checked once you augment Register:
    declare module "@kanzo-tech/ui" { interface Register { sections: typeof SECTIONS } }.
  • KanzoThemeProvider's sections takes a readonly array.

@kanzo-tech/ui

CommandDialog opens on a key: hotkey="mod+k" toggles the palette on ⌘K or Ctrl+K, controlled
or not, and leaves a key pressed while typing in a field to the field. There is no default, so a
palette you already wire to ⌘K by hand keeps working; delete your listener and pass hotkey instead.

CommandInput draws children before its input, where a palette that filters puts its chips.
/docs/actions/command shows the composition with useTagsInput.

Inside CommandDialogContent the list fills the dialog instead of stopping at the inline palette's
height.

PreferencesRoot's hotkey takes the same grammar — a bare key as before, or a chord such as
"mod+,".

⌘B no longer toggles the sidebar while you type in a field, so it stays bold in a rich-text editor.

@kanzo-tech/ui

Suggestion takes a description. Why a pill is on offer, a saved filter's note or a model's
rationale, goes in its tooltip and is read as its accessible description. The tooltip then opens
whether or not the label is cut: under the whole label when it is cut, alone when it is not.

@kanzo-tech/ui

Sections: no version, a when, and lists named by their section. Breaking.

  • SectionManifest has no version. Delete it from every section you declare; a stored value the
    manifest no longer offers already falls back to its default, so there is nothing to migrate.
  • A preference may declare when: { pref, eq } or { pref, neq } — offered only while a sibling
    resolves to that value. prefShown(decl, values) reads it.
  • PrefSource is any name, scoped to the section that declares it: { from: "columns" } is answered
    by the section's owner. The core's "themes" is unchanged.

@kanzo-tech/theme

GraphSearch is a ⌘K palette. In the page it is now a button styled as a field that prints ⌘K;
clicking it or pressing ⌘K / Ctrl+K opens the search centred and full-height over the canvas. type:
and <column>:<value> words become chips on Space, Enter goes to the highlighted vertex and closes,
⌘Enter selects every match. Empty, it lists your recent picks and every vertex type with its count,
and picking a type makes it a chip; each match shows its IRI's local name on the right, so matches
with the same title can be told apart. placeholder, size and limit are unchanged; className and size
now style the button, and size is "sm" | "md" | "lg". Mount one per page: the key opens every
palette that declares it.

GraphInspector is compact. It heads with the vertex's type over its title or IRI local name,
with a labelled Locate button beside them in place of the zoom, focus and copy icons, and prints
the IRI under the name with its copy button, so there is no Identity section. It no longer lists or
selects the neighbourhood — a click on the canvas already does — lays the fields out in two columns, and drops its card, so a dock no longer draws a
second border around it. Rows your render prop adds line up without classes of their own: drop any
you set on its DataListItems.

reveal frames the vertex with its neighbours (Locate, and Enter in the search), and the camera
stays there: a resize or a settling layout no longer puts the whole graph back.

Add @kanzo-tech/graph/tailwind.css to your stylesheet, after @kanzo-tech/ui's:

@import "tailwindcss";
@import "@kanzo-tech/ui/tailwind.css";
@import "@kanzo-tech/graph/tailwind.css";

Without it, classes only the graph's parts use are never generated — the search palette, for one,
collapsed to a bare input.

@kanzo-tech/graph

The graph's settings are its section, drawn by @kanzo-tech/ui. Breaking.

  • GraphLooks and GraphPlacement are gone, with their props types. The whole settings tab is now
    one line under the root, and the root supplies the corpus's columns and the cards' pictures itself:

    const { look, sim, placement } = useGraphPrefs();
    <GraphRoot look={look} sim={sim} {...placement} fill="kind" …>
      <PreferencesSections namespace="graph" />
    </GraphRoot>
  • Placement is a preference now: GRAPH_SECTION declares placement (Force · Map · Clustered),
    x-by, y-by and cluster-by. Drop the useState that held x/y/cluster and spread
    useGraphPrefs().placement on the root instead. A column a corpus does not carry is read as
    unbound, so the layout runs.

  • The three named looks are gone: PRESETS, presetOf and LookPreset are no longer exported, and
    useGraphPrefs() returns { look, sim, placement }. A host that paired channels with a look binds
    them once on the root.

  • New: placementFrom and the Placement type, beside lookFrom and simFrom.

  • Additive links is offered only while edges are drawn, and Cluster pull only under Clustered.

Closes #79.

@kanzo-tech/graph

One type for everything a model offers: Proposal. A suggested question and a field's candidate
are the same { text, rationale?, range? } now.

Breaking:

  • SuggestedQuestion is removed. suggest() and dataSuggestions() yield Proposals: read
    .text where you read .question.
  • Chat's suggestions takes readonly Proposal[] instead of readonly string[]. Pass the
    proposals suggest() yields as they are, or wrap a fixed list: questions.map((text) => ({ text })).
  • A model mocked for Assist's candidates answers { elements: [{ text, rationale }] }, not
    { value, rationale }.

@kanzo-tech/ai

Assist's candidates and Chat's questions are one strip. A ✨ leads it, and each pill's
rationale is now in its tooltip and read as its accessible description, in Chat too. While
Assist asks, the strip shows pills in skeleton instead of a spinner.

dataSuggestions offers what crosses a join first. It now favours questions across a join the
schema declares, and each rationale names the tables the question connects. Its docs and suggest()'s
now use your complete alias rather than chat.

Breaking: AssistTranslations.thinking is removed, since nothing says "Thinking…" any more.
Delete it from your translations. A candidate no longer carries a title; read its rationale from
the tooltip or the pill's aria-describedby.

@kanzo-tech/ai

useAgentChat(agent) runs an agent in the page and keeps what it threw. Its error is the value
the agent threw, so an AiError arrives with its code and data rather than as a sentence.
Replace useChat({ transport: new DirectChatTransport({ agent }) }) with useAgentChat(agent).
useChat stays for an agent behind your own HTTP route.

Chat draws its error through Problem, with the code shown, and takes a copy prop for your
words per code.

A call left running when an answer stopped reads "Stopped" instead of spinning forever. Tool
takes stopped for a transcript of your own.

QueryResult draws every state of a query call. While it runs, it shows the statement under a
skeleton. A stopped call shows its statement. A refusal is a Problem, and the statement gate's
carries query/refused on data-code. Pass it stopped from Chat.

Breaking:

  • ChatToolRenderers are called at every state of a call, not only once it has a result, with
    a second argument { stopped }. A renderer that only draws the result now checks
    part.state === "output-available" and returns something else before that, for example
    <ToolInput />.
  • Chat's chat.error is unknown, whatever stopped the answer.
  • A refusal in QueryResult is no longer an Alert: select it by [data-code="query/refused"] or
    [data-slot=diagnostic].

@kanzo-tech/ai

Chat and ChatSkeleton fill their parent's height, and land in the same place. Both are now
one frame: in a flex column or in a block with a height of its own, the skeleton is exactly as tall
as the chat that replaces it, with its empty state and composer in the same place. If you wrapped
ChatSkeleton in a flex flex-col box only so it would match Chat, you can drop the wrapper.

Behaviour change: suggestions={[]} keeps the strip's row. When suggesting fails, pass an empty
array as before; the strip now stays one pill high and empty, so the empty state above it does not
move. To draw no strip at all, leave suggestions out. While suggesting, the strip holds exactly
as many skeleton pills as it lacks, never a second row.

@kanzo-tech/ai

stream, the one door to a model's answer. stream({ model, system, prompt, output, abortSignal })
reads one answer as text, as elements (an Output.array, each once it is whole) or as partial
(an Output.object as it grows). Each reading throws what stopped the model once it ends, and
the request is never retried.

Breaking: streamText is no longer re-exported. Replace

const result = streamText({ model, prompt, output, onError });
for await (const e of result.elementStream) …

with

for await (const e of stream({ model, prompt, output }).elements) …

and drop the onError: the failure now arrives as a thrown error from the loop.

A gateway's 429 is now an AiError coded ai/rate-limited, with data.retryAfter in seconds
when the gateway sent retry-after or x-ratelimit-reset. It used to surface as the provider's
uncoded error. AiError["code"] is now "ai/silent" | "ai/rate-limited": a switch over it that
must be exhaustive needs the new case.

@kanzo-tech/llm

The AI SDK's Agent and ToolSet types are re-exported, for code that takes an agent, as useAgentChat does.

@kanzo-tech/llm

relaySelection(from, to, { except, onArrive, onWithdraw }) relays one selection's clauses into
another, as the clause objects themselves, so a chart is still exempt from its own clause. It returns
a function that stops the relay and withdraws everything it relayed. MosaicProvider and
bridgeSelection now use it, and nothing in the family reaches a private member of Mosaic.

clauseParts(clause) and clauseLabel(clause) say what a clause filters on, in words: the field,
the operator and the value, and the chip label they make.

Breaking: column is removed. Read an Arrow answer with numbers(data, field), which now reads
through mosaic-core's toDataColumns and throws the answer has no column "<field>" for a field the
query did not select. For any other column type, call toDataColumns from @uwdata/mosaic-core.

The DuckDB-WASM engine now uses DuckDBWASMConnector instead of the deprecated wasmConnector.
Nothing changes for you.

@kanzo-tech/mosaic

engine() ships DuckDB's parquet and json extensions and fetches nothing from
extensions.duckdb.org.
DuckDB-WASM builds in neither, so DuckDB fetched parquet from
extensions.duckdb.org on the first Parquet read and json on the @kanzo-tech/ai/data statement
gate's first question (it parses through json_serialize_sql) — requests a page under
connect-src 'self' <storage> refuses, and the read or the gate with them. Both now ship in
@kanzo-tech/mosaic beside httpfs, pinned by hash and emitted by your bundler as
parquet.duckdb_extension.<hash>.wasm and json.duckdb_extension.<hash>.wasm; nothing to
configure, and no new origin in your policy. DuckDB's autoloading is off: a function from an
extension the engine did not load fails with DuckDB's error naming it. A coordinator you build over
another DuckDB for the gate needs json loaded itself.

@kanzo-tech/mosaic, @kanzo-tech/ai

An API now receives a token for itself and one organization, never the session's own token.
Breaking for a product that configures api or calls token or refresh with renewWithin.

  • kanzoAuth's api: { mount, target } is now apis, keyed by mount, and each entry names its
    audience: apis: { "/api": { audience: "board-api", target } }. One route file
    (app/api/[...path]/route.ts exporting auth.api) serves every mount; the longest mount wins.
    The forwarder exchanges the session's token (RFC 8693, Keycloak's standard token exchange) for one
    whose aud is that API and whose organization is the one the request addresses. It answers 403
    when the person is not a member of that organization, and 502 when the realm will not exchange
    for the audience.
  • relyingParty().token(cookie, options) takes { audience, organization?, renewWithin? } and
    answers the exchanged token. The session's own access token is no longer returned by any method.
  • refresh(cookie, { renewWithin }) renews only when the session is inside that window; without
    it, refresh renews now, as before. Use it where you called token only to keep a session alive.
  • New error code organization/denied.
  • In the realm, register each API with services/auth/modules/api and list it in the
    application's modules/app apis, which replaces audience. The application's client must be
    CONFIDENTIAL. Your API keeps validating aud against its own client id, and keeps reading
    roles where it did.

@kanzo-tech/auth

The gateway in services/ai is agentgateway, and the realm's token is its only credential.
Breaking for a deployment of services/ai; createGateway and the aliases chat and complete are
unchanged.

  • Your server no longer presents a tenant's key. It forwards a token exchanged for the gateway
    (ai-gateway) and the one organization the request is for: add
    "/api/ai": { audience: "ai-gateway", target: AI_GATEWAY_URL } to kanzoAuth's apis, and
    "ai-gateway" to your application's modules/app apis. A token naming every membership is
    refused with 403.
  • modules/team is gone, with the per-tenant keys and teams: delete your instances of it. The
    gateway charges the organization the token names.
  • modules/gateway takes issuer (the realm, as its tokens say it), optionally jwks_url and
    audience, and tokens_per_hour for each organization's budget. profile is now YAML with one
    key, models, in agentgateway's llm.models shape, and upstream keys are read as $NAME. The
    admin_* variables and the master_key output are gone.
  • Development: docker compose -f ../auth/compose.yml -f compose.yml up -d --wait; the gateway
    trusts the identity service beside it. Requests are logged to Postgres with the person, the
    organization, the tokens and their cost. The opt-in cache for complete is gone.

@kanzo-tech/llm