Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ pnpm add --save-dev react@^18.2.0 react-dom@^18.2.0 @types/react@^18 @types/reac
4. Add it to `ITEMS` in `scripts/registry.mjs` and run `pnpm registry:generate`, which works out the files and dependencies from its imports.
5. Show it on the components page (`examples/playground/app/components/page.tsx`, with a demo in `examples/playground/components/demos/`), and document it on the docs site: an entry in `examples/playground/lib/components.ts` and how it behaves in `examples/playground/content/components/<slug>.md`. The README lists components in one table; add it there if it is one people should know about.

## New integrations

Until Thursday 2026-11-19, four weeks after the public launch on 2026-10-22, the kit adds no new integration: no new chat kit, protocol adapter or agent SDK, whether as a package entry, a registry item, an example or a docs page. Fixes, docs and tests for the integrations it has go on as usual.

The exception is an integration someone needs and asks for in an issue. Open a [feature request](https://github.com/dgesteves/signoff-ui/issues/new?template=feature_request.yml) that says what you are building and what it runs on (the chat kit, protocol or agent SDK, and its version), or add your case to an open one. After those four weeks, propose a new integration in an issue first, as any larger change.

Each integration is in a [support tier](https://signoff-ui.vercel.app/docs/limits#support-tiers): Tier 1 when a CI job runs it on every pull request, Tier 2 when it is documented but not verified. A new one is Tier 2 until a CI job runs it.

## Changesets

A change to the published package needs a changeset: run `pnpm changeset`, pick `signoff-ui`, choose patch for a fix or minor for a feature, and write a line or two for the changelog from a user's point of view. Docs, the examples and CI need none. Merging to `main` opens a "Version packages" pull request, and merging that publishes to npm. Its version step, `pnpm version-packages`, also moves `examples/coding-agent` to `signoff-ui@^<new version>`, so a copy of the example installs the release its code is written for. [RELEASING.md](./RELEASING.md) covers the release workflow, its dry run, and what to do when a release step fails.
Expand Down
9 changes: 8 additions & 1 deletion examples/playground/content/docs/limits.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
What the components cost, where they slow down, and what they don't do yet. The numbers are measured from this repository; the commands that measure them are named next to each.
What the components cost, where they slow down, what they don't do yet, and what is tested. The numbers are measured from this repository; the commands that measure them are named next to each.

## Bundle size

Expand Down Expand Up @@ -93,6 +93,13 @@ Measured with `pnpm perf`, with a budget per case; the append's is 16 ms, under
- **Text that reads as something else.** A command, its `cwd`, a file path and a diff line show the characters that do not show (bidi controls such as U+202E, zero-width spaces, Unicode tags, control characters such as an ANSI escape) as a mark with their code points. A command wraps instead of scrolling sideways, so no filler before its end can hide that end to the side: a long command makes the card taller. A command or a path also shows a run of spaces 16 columns wide or more as its count, and a run of 3 blank lines or more as the number of its line breaks, so blank lines cannot push that end out of view below; spaces other than U+0020 show by their code points. The joiners and variation selectors inside an emoji are left as they are, and a variation selector after a letter is marked. Lines with something on them, even a dot or a lone accent each, still make the card taller, as a long command does. A Markdown link whose text names another host than the one it goes to, anywhere in it (`[Visit github.com →](https://evil.example)`), or names it in look-alike letters, or whose URL puts a name before the host (`https://github.com@evil.example`), shows that host beside it. Any word that reads as a domain names one, so `JSON.parse` shows the host too, unless it is a file's name: `Next.js`, or `README.md` when the link goes to that file. So does an IP address (`8.8.8.8`), and so a version in four parts such as `1.2.3.4`; a domain before an `@` (`github.com@evil.example`), and so, on a link that is not mailto, an email address whose name has a dot (`jane.doe@github.com`); and a domain written with a character that reads as a dot: the full stops a URL reads as dots (`github。com`, `github.com`), the one dot leader and the small full stop (`github․com`, `github﹒com`), the raised and middle dots (`github·com`), the Syriac and Vai full stops, and between Latin letters the Arabic zeros (`github٠com`) and the Lisu tone mark `ꓸ`. A stop beside a Chinese, Japanese, Korean, Syriac or Vai letter is left as it is, since it ends a sentence there, and so is a middle dot between two l's, as in Catalan (`col·legi`); between other letters a middle dot reads as a dot, so a word split into syllables (`pro·nun·ci·a·tion`) shows the host too. `Sources`, file attachments and Markdown link only to http, https, mailto and relative URLs, and an attached file to `data:` and `blob:` too: anything else is text, not a link. Not covered yet: arguments shown as JSON (a tool without a `command`) and tool output, which show those characters as they are; the argument fields while editing; look-alike letters in a command; link text that reads as a file name and as a domain (`docs.rs`, `install.sh`) when the link goes to a file of that name; a domain in Chinese, Japanese or Korean written with `。` (`例え。jp`); other characters that look like a dot, such as the hyphenation point (U+2027) and the dot operator (U+22C5); and your own `preview` or Markdown renderer, which can call `revealText` and `safeUrl` from `signoff-ui/core`.
- **Nesting in slots.** With `styles.css`, a component rendered in a slot of a component that is itself in a slot is styled up to two levels deep; deeper than that it renders unstyled.

## Support tiers

What the kit works with is in one of two tiers. [Works with](/docs/getting-started#works-with) in Getting started has the matrix: its "Tested in CI" column is Tier 1, and its "Should work, not tested in CI" column is Tier 2.

- **Tier 1: tested in CI.** A job in [`ci.yml`](https://github.com/dgesteves/signoff-ui/blob/main/.github/workflows/ci.yml) runs it on every pull request, so a change that breaks it fails that pull request's checks. The jobs, by the names the checks show: **Lint, typecheck, test, build**, **AI SDK 6**, **React 18**, **Next.js quickstart**, **assistant-ui binding**, **CopilotKit example**, **Coding agent reference app**, **WebKit (Safari's engine)** and **Firefox**. Works with says what each one runs. A report that a Tier 1 entry broke is a bug, and comes first.
- **Tier 2: documented, not verified.** The docs say how it connects, through the same message parts or as plain React, but no CI job runs it, so a release can break it without a check failing: AI Elements, Mastra, Cloudflare Agents, Vercel Eve, LangGraph, Next.js 14 and 15, and the rest of that column. So is anything the docs name that the matrix doesn't list, such as Pydantic AI, and each real ACP agent, since CI runs the ACP adapter on scripted turns. A report that a Tier 2 entry doesn't work is welcome, with the versions you ran; it is fixed as time allows, and an entry moves to Tier 1 once a CI job runs it.

## What's next

The next work, with its issue where it has one:
Expand Down
120 changes: 120 additions & 0 deletions packages/signoff-ui/test/support-tiers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// @vitest-environment node
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { headingId, section } from './utils';

/*
* Support tiers, on the limits page, and the freeze on new integrations in CONTRIBUTING.md. Tier 1
* names the CI jobs that run on every pull request, so it can't outlive or miss one; both tiers
* point at the "Works with" matrix in Getting started rather than keep a second one. The freeze
* names the day it ends, four weeks after the 2026-10-22 launch.
*/

const root = fileURLToPath(new URL('../../../', import.meta.url));
const read = (path: string) => readFileSync(join(root, path), 'utf8');
const limits = read('examples/playground/content/docs/limits.md');
const guide = read('examples/playground/content/docs/getting-started.md');
const contributing = read('CONTRIBUTING.md');
const ci = read('.github/workflows/ci.yml');

const tiers = section(limits, 'Support tiers');
/** A bulleted item, from its bold lead to the end of its line. */
const item = (markdown: string, lead: string) =>
markdown.split('\n').find((line) => line.startsWith(`- **${lead}`)) ?? '';
const tier1 = item(tiers, 'Tier 1: tested in CI.');
const tier2 = item(tiers, 'Tier 2: documented, not verified.');

/** The names of ci.yml's jobs that run on every pull request: their own `if` skips only the daily run. */
const pullRequestJobs = ci
.slice(ci.indexOf('\njobs:\n'))
.split(/\n {2}(?=[\w-]+:\n)/)
.slice(1)
.filter((job) => /^ {4}if: github\.event_name != 'schedule'$/m.test(job))
.map((job) => /^ {4}name: (.+)$/m.exec(job)?.[1] ?? '');

/** The "Works with" matrix in Getting started: its header and each row's two columns, as text. */
const matrix = section(guide, 'Works with')
.split('\n')
.filter((line) => line.startsWith('|'))
.map((line) =>
line
.slice(1, -1)
.split('|')
.map((cell) => cell.trim()),
);
const [header = []] = matrix;
const tested = matrix
.slice(2)
.map((row) => row[1])
.join(', ');
const untested = matrix
.slice(2)
.map((row) => row[2])
.join(', ');

/** What Tier 2 names from the matrix's untested column. */
const TIER_2 = ['AI Elements', 'Mastra', 'Cloudflare Agents', 'Vercel Eve', 'LangGraph', 'Next.js 14 and 15'];

describe('Support tiers', () => {
it('is a section of the limits page, with a tier for what CI tests and one for what it does not', () => {
expect(tiers).not.toBe('');
expect(tier1).not.toBe('');
expect(tier2).not.toBe('');
});

it('links the Works with matrix, and calls its columns by their headers', () => {
expect(section(guide, 'Works with')).not.toBe('');
expect(tiers).toContain(`[Works with](/docs/getting-started#${headingId('Works with')})`);
expect(header.slice(1)).toEqual(['Tested in CI', 'Should work, not tested in CI']);
expect(tiers).toContain('its "Tested in CI" column is Tier 1');
expect(tiers).toContain('its "Should work, not tested in CI" column is Tier 2');
// One matrix, in Getting started: no table of its own here.
expect(tiers).not.toMatch(/^\|/m);
});

it('names in Tier 1 every CI job that runs on each pull request, and no other', () => {
expect(pullRequestJobs).toContain('AI SDK 6');
expect(pullRequestJobs).toContain('CopilotKit example');
expect(pullRequestJobs).toContain('Firefox');
expect(pullRequestJobs).toContain("WebKit (Safari's engine)");
const named = [...tier1.matchAll(/\*\*([^*]+)\*\*/g)].map((match) => match[1]).slice(1);
expect([...named].sort()).toEqual([...pullRequestJobs].sort());
expect(tier1).toContain('(https://github.com/dgesteves/signoff-ui/blob/main/.github/workflows/ci.yml)');
});

it('names in Tier 2 only entries the matrix gives as untested, and what it does not list', () => {
for (const name of TIER_2) {
expect(tier2, name).toContain(name);
expect(untested, name).toContain(name);
expect(tested, name).not.toContain(name);
}
// The docs name Pydantic AI among the AG-UI frameworks; CI runs the ACP adapter on scripted turns.
expect(read('examples/playground/content/docs/ag-ui.md')).toContain('Pydantic AI');
expect(untested).not.toContain('Pydantic AI');
expect(tier2).toContain('Pydantic AI');
expect(tier2).toContain('scripted turns');
});
});

describe('CONTRIBUTING: new integrations', () => {
const freeze = section(contributing, 'New integrations');

it('holds new integrations until four weeks after the public launch, unless an issue asks for one', () => {
expect(freeze).not.toBe('');
expect(freeze).toContain('Until Thursday 2026-11-19, four weeks after the public launch on 2026-10-22');
for (const kind of ['chat kit', 'protocol adapter', 'agent SDK']) expect(freeze, kind).toContain(kind);
expect(freeze).toContain('The exception is an integration someone needs and asks for in an issue.');
});

it('says how to ask, through the feature request form', () => {
expect(existsSync(join(root, '.github/ISSUE_TEMPLATE/feature_request.yml'))).toBe(true);
expect(read('.github/ISSUE_TEMPLATE/feature_request.yml')).toContain('integration');
expect(freeze).toContain('(https://github.com/dgesteves/signoff-ui/issues/new?template=feature_request.yml)');
});

it('links the support tiers on the limits page', () => {
expect(freeze).toContain(`(https://signoff-ui.vercel.app/docs/limits#${headingId('Support tiers')})`);
});
});
Loading