Repository navigation
v0.31.0
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)replacesmigrateDashboards, andparseDashboard(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>: ….nullorundefinedreads as no dashboards. Nothing is
migrated: a spec saved by an earlier release is refused, so catch the error and delete what you
stored.DashboardSpecandDashboardshave noversionfield. Stop writing one.onChangeis called withundefinedon "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.TileEditoris a popover beside the tile it edits, and it draws no tile.tileis now the
draft (aTile, no longerTile | null) andonChangereceives every change to it: draw that
draft in the tile's own view, and pass that view's element asanchor: () => HTMLElement | null.
tableandconfigare gone. Mount it only while a tile is being edited.Dashboardalready does
all of this.- A figure (
StatTile) has nospan. 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
theirspanin the grid below. Dropspanfrom everykind: "stat"tile you write; a stored spec
that still has one is refused byparseDashboard. columnis no longer exported. Usenumbers(data, field).
New:
Recommendation.rulenames 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(noonChange) 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.
PreferencesSectionslosesonlyandspecimens. Write the parts instead:
<PreferencesSections namespace="theme" only={["density"]} />becomes<Pref name="theme.density" />,
andspecimens={{ "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.SectionProviderlets a section's owner answer the lists its choices name and the pictures its
options wear, for the subtree beneath it.KanzoThemeProvideris the core's;useKanzoTheme()no
longer returnssources.- 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'ssectionstakes areadonlyarray.
@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.
SectionManifesthas noversion. 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. PrefSourceis 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.
-
GraphLooksandGraphPlacementare 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_SECTIONdeclaresplacement(Force · Map · Clustered),
x-by,y-byandcluster-by. Drop theuseStatethat heldx/y/clusterand spread
useGraphPrefs().placementon 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,presetOfandLookPresetare no longer exported, and
useGraphPrefs()returns{ look, sim, placement }. A host that paired channels with a look binds
them once on the root. -
New:
placementFromand thePlacementtype, besidelookFromandsimFrom. -
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:
SuggestedQuestionis removed.suggest()anddataSuggestions()yieldProposals: read
.textwhere you read.question.Chat'ssuggestionstakesreadonly Proposal[]instead ofreadonly string[]. Pass the
proposalssuggest()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:
ChatToolRenderersare 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'schat.errorisunknown, whatever stopped the answer.- A refusal in
QueryResultis no longer anAlert: 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'sapi: { mount, target }is nowapis, keyed by mount, and each entry names its
audience:apis: { "/api": { audience: "board-api", target } }. One route file
(app/api/[...path]/route.tsexportingauth.api) serves every mount; the longest mount wins.
The forwarder exchanges the session's token (RFC 8693, Keycloak's standard token exchange) for one
whoseaudis 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,refreshrenews now, as before. Use it where you calledtokenonly to keep a session alive.- New error code
organization/denied. - In the realm, register each API with
services/auth/modules/apiand list it in the
application'smodules/appapis, which replacesaudience. The application's client must be
CONFIDENTIAL. Your API keeps validatingaudagainst 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 }tokanzoAuth'sapis, and
"ai-gateway"to your application'smodules/appapis. A token naming every membership is
refused with 403. modules/teamis gone, with the per-tenant keys and teams: delete your instances of it. The
gateway charges the organization the token names.modules/gatewaytakesissuer(the realm, as its tokens say it), optionallyjwks_urland
audience, andtokens_per_hourfor each organization's budget.profileis now YAML with one
key,models, in agentgateway'sllm.modelsshape, and upstream keys are read as$NAME. The
admin_*variables and themaster_keyoutput 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 forcompleteis gone.
@kanzo-tech/llm