This file is the single source of truth for how to write code in this package. Rules are non-negotiable unless flagged as a heuristic.
This is the TypeScript sibling of the django-* Python packages and follows the same
shared standard, with the toolchain swapped for the JS/TS ecosystem:
| Concern | Python packages | This package |
|---|---|---|
| Package manager / build | uv + hatchling | pnpm + esbuild |
| Lint + format | ruff | Biome |
| Type check | ty | tsc --noEmit |
| Test + coverage | pytest + pytest-cov | Vitest + @vitest/coverage-v8 |
| Version bump | bump-my-version | scripts/release-bump.mjs |
| Publish | PyPI OIDC | npm OIDC (provenance) |
The Makefile target names are identical across all packages (init/test/lint/lint-fix/
format/format-check/type-check/build/release-bump/release-publish).
A framework-free <ag-ui-chat> Web Component over the AG-UI
protocol. Wraps @ag-ui/client's HttpAgent. Ships:
- The Custom Element with a Shadow DOM chat UI.
- A pluggable client-side tool registry (
registerTool({ name, description, parameters, handler })); registered tools are added to everyRunAgentInput.tools. - Generic DOM driver primitives (
fillField,clickElement,typeInto, …) and animations. - An inline confirmation card that intercepts tool calls needing confirmation — driven by the
x-destructive: trueJSON-Schema flag (or per-callconfirmPredicate), with prompt text fromx-confirm.
Downstream consumers (e.g. django-admin-agent) register their own admin-aware tool handlers
on top via the pluggable registry. No Django/admin specifics live here.
The AG-UI stack design doc (django-ag-ui-plan.md) lives in the private ecosystem planning workspace, outside this repo.
| Target | What it does |
|---|---|
make init |
pnpm install + install pre-commit hooks |
make test |
Vitest with 100% line+branch+function+statement coverage gate |
make lint |
biome check . + tsc --noEmit |
make format |
biome format --write . |
make build |
esbuild bundle + tsc declarations into dist/ |
make release-bump VERSION=X.Y.Z |
rewrite src/version.ts + package.json + CHANGELOG |
make release-publish |
end-to-end workstation release |
- One exported class or function per file. File name =
snake_caseorkebab-caseof the symbol's concept.AgUiChatCustom Element →ag_ui_chat.ts;fillField→fill_field.ts. Keep the file-per-symbol discipline from the Python packages. Exception:src/constants.tsis the single home for enums and constant-like values, and is the only file allowed to export multiple symbols. - Private helpers used in only one file stay there, not exported.
- Non-exported helpers shared across files go into a sibling
utils.ts. - Top-level imports only. No dynamic
import()unless genuinely needed for code-splitting a heavy optional dependency, documented inline. - Full type annotations on every exported function and method signature.
anyis forbidden except at genuine external-boundary points, and even there preferunknown.tsconfigruns in fullstrictmode plusnoUncheckedIndexedAccess,exactOptionalPropertyTypes,noPropertyAccessFromIndexSignature. src/index.tsis the only re-export point. It lists the public surface. Internal modules import from leaf paths (./fill_field.ts), never from./index.ts.- Use
.jsextensions in import specifiers (import { x } from "./x.js") even though the source file isx.ts— NodeNext convention. tsc, esbuild, and Vitest all resolve.js→.ts/.d.tscorrectly, and the emitted.d.tskeeps./x.js, so consumers resolve types without needingallowImportingTsExtensions. Never use bare extensionless imports or.tsextensions (the latter forces the flag onto every downstream consumer). import typefor type-only imports (Biome enforcesuseImportType).src/is grouped one level deep by concern —core/(the element, AG-UI client, conversation store),ui/(rendered widgets + styles + markdown/word rendering),dom/(host-page driving: animations, dom driver, native setter),tools/(client tool registry, route/page maps, state hook, schema predicates), andskills/(skill type, templating, parsing).index.ts,constants.ts, andversion.tsstay at thesrc/root. One level only — no deeper nesting. Cross-group imports use relative../<group>/x.jspaths; same-group imports use./x.js. Tests stay flat undertests/(named after the source symbol).
- Frozen, explicitly-typed data shapes. Wire payloads and config records are
readonlyinterfaces oras constobjects with explicit field types — never untyped object literals passed across module boundaries. - Tool schemas are JSON Schema. The
x-destructiveflag lives at the schema root. The inline confirmation card reads it; the registry forwards it verbatim toRunAgentInput.tools. Don't invent a parallel metadata channel.
State lives on Custom Element instances (private class fields) — never at module scope. No
module-level mutable singletons, caches, or "warned-once" flags. Each <ag-ui-chat> element
owns its own tool registry, AG-UI client, and Shadow DOM. Multiple instances on one page must
not interfere.
make testruns Vitest with 100% line + branch + function + statement coverage (thresholds invitest.config.ts). Restructure rather than carve out coverage exclusions.- Test layout mirrors
src/undertests/.src/fill_field.ts→tests/fill_field.test.ts. - DOM tests run under
happy-dom(configured as the Vitest environment). Custom Element registration, Shadow DOM queries, and event dispatch all work there. - For AG-UI protocol behaviour, mock
@ag-ui/client'sHttpAgentrather than hitting a real server; assert on theRunAgentInputshape produced and the handling of synthetic events.
make lintrunsbiome check .+tsc --noEmit. CI fails on either.- Biome is the source of truth for both lint and format.
- Pre-commit runs
make lint-fix,make format,make type-check. Commits must be clean before push — never--no-verify.
| Component | Floor | Tested |
|---|---|---|
| Node (tooling/tests) | 22 | 22, 24 |
| Browsers (runtime target) | ES2022 / evergreen | Chrome/Firefox/Safari 17+ |
@ag-ui/client |
latest 0.x | — |
The shipped artefact targets evergreen browsers (Shadow DOM, Custom Elements v1, ES2022). Node is only the build/test runtime, not a runtime target.
When working on a new feature or version bump, ALWAYS branch first (git checkout -b feat/... or release/vX.Y.Z) and push there. Never commit feature work or version bumps
directly to main; main only advances via merged PRs (or, for releases, the tagged commit
on the release branch).
Merge-to-main triggered. .github/workflows/release.yml runs on every push to main and
calls make release-publish-prepare. The script in scripts/release-publish.sh is the single
source of truth (a near-byte-identical port of the Python packages' script):
- Extract version from
src/version.ts; assertpackage.jsonagrees. - Short-circuit if
vX.Y.Zalready exists locally or on origin. pnpm testas a final gate.pnpm buildintodist/.- Extract the
## [X.Y.Z]CHANGELOG section into release notes. - Emit
released=true.
If released:
- Publish to npm via OIDC trusted publishing with provenance (
pnpm publish). - Tag, push, create GitHub Release.
make release-bump VERSION=0.2.0 # rewrites src/version.ts + package.json + CHANGELOG
git diff
git commit -am "Release 0.2.0"
git push -u origin release/0.2.0
gh pr create
# Merge to main; release.yml fires on the merge commit.release-bump.mjs refuses to run on a dirty tree.
- npm Trusted Publisher — configure OIDC trusted publishing for
@artooi/ag-ui-web-componentpointing atArtui/ag-ui-web-component, workflowrelease.yml, environmentnpm. @artooinpm org — must exist and the publishing identity must be a member.- GitHub Environment — create an
npmenvironment underSettings → Environments(no secrets needed; OIDC handles auth).