From 829fe44212a65dfde487852bdd1172363e1fec26 Mon Sep 17 00:00:00 2001 From: Rasmus Viloma Date: Sun, 11 Oct 2026 23:50:12 +0300 Subject: [PATCH] chore(release): write concise changelog lines with primitive tags --- .changeset/README.md | 20 +++ .changeset/changelog.cjs | 10 ++ .changeset/config.json | 2 +- .changeset/forty-symbols-dream.md | 2 + AGENTS.md | 2 + bun.lock | 19 +-- content/docs/changelog.mdx | 3 +- lib/docs/remark-changelog-tags.ts | 62 ++++++++ package.json | 1 - packages/chat/CHANGELOG.md | 225 ++++-------------------------- source.config.ts | 2 + 11 files changed, 133 insertions(+), 215 deletions(-) create mode 100644 .changeset/changelog.cjs create mode 100644 .changeset/forty-symbols-dream.md create mode 100644 lib/docs/remark-changelog-tags.ts diff --git a/.changeset/README.md b/.changeset/README.md index 43edd65..6fb1b2a 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -11,6 +11,26 @@ Pick the bump (major / minor / patch) and write a summary — it becomes the cha entry. On merge to `main`, the release workflow opens (or updates) a "Version Packages" PR; merging that PR publishes to npm. +## Writing the summary + +`changelog.cjs` prints the summary as-is, so write only what a user of the package needs: + +- One line per change: what changed, not how or why. The reasoning goes in the PR body. +- Lead with the primitive in brackets (`[Thread]`), shown as a badge in the docs. Leave it off + a line that isn't about one primitive. +- Then a verb (Add, Fix, Improve, Remove) or "New", with API names in code spans. +- A breaking change adds a `[Breaking]` tag and the migration in one sentence. Before 1.0 it + ships as a minor bump, so the tag is what marks it. + +```md +--- +"@intentface/chat": minor +--- + +- [Composer] Fix an Escape in one composer stopping generation in another. +- [Composer] [Breaking] Escape stops generation only from inside `Composer.Root`. To stop on any Escape, handle it on your chat container, such as `Thread.Root`. +``` + If a package change intentionally needs no release, add an empty changeset instead: ```bash diff --git a/.changeset/changelog.cjs b/.changeset/changelog.cjs new file mode 100644 index 0000000..7b1002c --- /dev/null +++ b/.changeset/changelog.cjs @@ -0,0 +1,10 @@ +// A release line is the changeset's summary alone: no PR link, commit hash or credit. +/** @type {import("@changesets/types").ChangelogFunctions} */ +module.exports = { + // A summary is one line, or a list of them (see README.md). + getReleaseLine: async ({ summary }) => { + const line = summary.trim(); + return line.startsWith("- ") ? line : `- ${line}`; + }, + getDependencyReleaseLine: async () => "", +}; diff --git a/.changeset/config.json b/.changeset/config.json index f5cfd27..f4c5626 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "Intentface/intentface-chat" }], + "changelog": "./changelog.cjs", "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/forty-symbols-dream.md b/.changeset/forty-symbols-dream.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/forty-symbols-dream.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/AGENTS.md b/AGENTS.md index 1cd9eb3..05337d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,8 @@ Two constraints follow, and both are load-bearing: This pattern is used throughout: `Message`, `Composer`, `Attachments`, `Chip`, `Thread`, `Steps`, `Reasoning`, `AskUser`. +A change to the package needs a changeset, and a changeset is one short line per change, not a write-up: see [`.changeset/README.md`](./.changeset/README.md). The reasoning goes in the PR body. + ### AI Integration The chat API follows Vercel AI SDK conventions diff --git a/bun.lock b/bun.lock index b1684a7..9c63c67 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,6 @@ }, "devDependencies": { "@biomejs/biome": "2.2.0", - "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.31.0", "@tailwindcss/postcss": "^4.3.2", "@types/mdx": "^2.0.14", @@ -48,7 +47,7 @@ }, "packages/chat": { "name": "@intentface/chat", - "version": "0.5.0", + "version": "0.5.2", "dependencies": { "@floating-ui/dom": "^1.7.6", "nanoid": "^5.1.11", @@ -120,8 +119,6 @@ "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], - "@changesets/changelog-github": ["@changesets/changelog-github@0.7.0", "", { "dependencies": { "@changesets/get-github-info": "^0.8.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-rBsbRvc4TVn+FvFnOVM3LxlFJfTXXCp8gfVJ+0BubxWNSVnLuAzowi5j+IEraLLP52w8AAs9QfKbPS3MMiXQJA=="], - "@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="], "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], @@ -130,8 +127,6 @@ "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="], - "@changesets/get-github-info": ["@changesets/get-github-info@0.8.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ=="], - "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.10", "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="], "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], @@ -628,8 +623,6 @@ "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], - "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -652,8 +645,6 @@ "dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="], - "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], - "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -966,8 +957,6 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], @@ -1152,8 +1141,6 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1198,12 +1185,8 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], diff --git a/content/docs/changelog.mdx b/content/docs/changelog.mdx index 5f9bec4..411941f 100644 --- a/content/docs/changelog.mdx +++ b/content/docs/changelog.mdx @@ -5,7 +5,8 @@ description: Every release of @intentface/chat, newest first. {/* Changesets writes packages/chat/CHANGELOG.md on every release; it is included here at build time, so this page is always the released notes. Its own - `# @intentface/chat` title is hidden, and the first version keeps no rule. */} + `# @intentface/chat` title is hidden, and the first version keeps no rule. + A line's leading [Tag]s become badges (lib/docs/remark-changelog-tags.ts). */}
diff --git a/lib/docs/remark-changelog-tags.ts b/lib/docs/remark-changelog-tags.ts new file mode 100644 index 0000000..0658aa5 --- /dev/null +++ b/lib/docs/remark-changelog-tags.ts @@ -0,0 +1,62 @@ +// CHANGELOG.md tags a line with plain-text brackets ("- [Thread] Fix …") so it +// reads on GitHub too. On the changelog page, the leading tags become badges. + +type MarkdownNode = { + type: string; + value?: string; + children?: MarkdownNode[]; + data?: Record; +}; + +const LEADING_TAGS = /^(?:\[\w+\] )+/; + +// Each tag's dot colour as an OKLCH hue. A tag missing here gets a grey dot. +const TAG_HUES: Record = { + Breaking: 25, + Nav: 65, + Shell: 95, + AskUser: 125, + Attachments: 150, + Steps: 175, + Message: 200, + Thread: 230, + Composer: 258, + Reasoning: 290, + Chip: 320, + Tabs: 345, +}; + +const badge = (name: string): MarkdownNode => { + const hue = TAG_HUES[name]; + return { + type: "emphasis", + data: { + hName: "span", + hProperties: { + className: + "mr-1.5 inline-flex h-5 items-center gap-1.5 rounded-[5px] bg-ink-primary/7 px-1.5 font-medium text-ink-primary text-xs before:size-1.5 before:rounded-full before:bg-[var(--tag-color,var(--color-ink-tertiary))]", + style: hue === undefined ? undefined : `--tag-color: oklch(0.68 0.14 ${hue})`, + }, + }, + children: [{ type: "text", value: name }], + }; +}; + +const tagListItem = (item: MarkdownNode) => { + const paragraph = item.children?.[0]; + const text = paragraph?.children?.[0]; + const tags = text?.type === "text" ? text.value?.match(LEADING_TAGS)?.[0] : undefined; + if (!paragraph?.children || !text?.value || !tags) return; + + text.value = text.value.slice(tags.length); + paragraph.children.unshift(...(tags.match(/\w+/g) ?? []).map(badge)); +}; + +const visit = (node: MarkdownNode) => { + if (node.type === "listItem") tagListItem(node); + node.children?.forEach(visit); +}; + +export const remarkChangelogTags = () => (tree: MarkdownNode, file: { path?: string }) => { + if (file.path?.endsWith("changelog.mdx")) visit(tree); +}; diff --git a/package.json b/package.json index 0346e76..271568d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ }, "devDependencies": { "@biomejs/biome": "2.2.0", - "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.31.0", "@tailwindcss/postcss": "^4.3.2", "@types/mdx": "^2.0.14", diff --git a/packages/chat/CHANGELOG.md b/packages/chat/CHANGELOG.md index bfe3fd2..f0eb48c 100644 --- a/packages/chat/CHANGELOG.md +++ b/packages/chat/CHANGELOG.md @@ -4,245 +4,82 @@ ### Patch Changes -- [#80](https://github.com/Intentface/intentface-chat/pull/80) [`07671d3`](https://github.com/Intentface/intentface-chat/commit/07671d3ac6931284d210d776b44f2848f78fe159) Thanks [@rpvilo](https://github.com/rpvilo)! - Documentation only, no runtime change. Every part of `Attachments`, `Chip`, `Message`, `Reasoning` and `Steps` now carries JSDoc saying what it is for and which element it renders, so editors show the same reference as the docs site. The package README is refreshed to match the rewritten docs. +- Add JSDoc to every part of `Attachments`, `Chip`, `Message`, `Reasoning` and `Steps`, so editors show the same reference as the docs. +- Refresh the README to match the rewritten docs. ## 0.5.1 ### Patch Changes -- [#85](https://github.com/Intentface/intentface-chat/pull/85) [`b49fb7a`](https://github.com/Intentface/intentface-chat/commit/b49fb7a0c2948beb46ed9c9864ee4ba676940147) Thanks [@rpvilo](https://github.com/rpvilo)! - `Thread` keeps the bottom pinned while its container is resized. - - **Resizing with a long last turn.** Dragging a chat window's height let the bottom drift out of view until the next content change re-pinned it, whenever the last turn was taller than its reserve: the content didn't change size, so nothing fired. The follow now observes the scroller as well as the content, so a resize re-pins in the same frame. - - **No smooth scroll queued behind a resize.** One resize settles over several observer passes in a frame: the scroller's box changes first, then the content resizes to the new reserve a pass later. That later pass read as content growth and queued a smooth scroll behind the instant re-pin. Re-pins now stay instant for the rest of a frame in which the scroller resized. - - New turns still land smoothly, streaming still follows smoothly, and scrolling up still releases the follow. +- [Thread] Fix losing the bottom while the container is resized. ## 0.5.0 ### Minor Changes -- [#83](https://github.com/Intentface/intentface-chat/pull/83) [`b50a489`](https://github.com/Intentface/intentface-chat/commit/b50a48964df5bce3a7f174ea7b2c881e69d72663) Thanks [@jonijuup](https://github.com/jonijuup)! - Escape now reaches only the thing it was meant for, found by putting a `Composer` inside a floating `Tabs.Popup`. - - **`Tabs`: an Escape something else handled no longer closes the panel.** `Tabs.Root`'s `dismissOnEscape` closed the open panel on every Escape, even one another handler had already used and marked with `preventDefault()`. A composer inside the popup stopping a reply, or closing its command list, closed the popup with the same press. Now each press peels one layer. - - **`Composer`: Escape stops only the composer it was pressed in.** While `isGenerating`, `Composer.Submit` (and `useComposerSubmit`) stopped on an Escape anywhere on the page. With two composers on a page, an Escape typed into one stopped the other, and an Escape in an unrelated text field stopped it too. The stop now runs from `Composer.Root`'s `onKeyDown`, so it hears only Escapes from inside that composer, including parts a positioned `Composer.Panel` renders elsewhere on the page. A consumer's `onKeyDown` can skip it with `event.preventPrimitiveHandler()`. - - **Breaking:** an Escape pressed after focus has left the composer (the page body, a button, the transcript) no longer stops generation. An app that wants that handles Escape on its chat's container, such as `Thread.Root`, and calls its own stop. +- [Tabs] Fix `dismissOnEscape` closing the panel on an Escape another handler already prevented. +- [Composer] Fix an Escape in one composer stopping generation in another. +- [Composer] [Breaking] Escape stops generation only from inside `Composer.Root`. To stop on any Escape, handle it on your chat container, such as `Thread.Root`. ## 0.4.0 ### Minor Changes -- [#81](https://github.com/Intentface/intentface-chat/pull/81) [`a7b9655`](https://github.com/Intentface/intentface-chat/commit/a7b9655ac7d0a2cd3571bd75683ec640f20ee01a) Thanks [@jonijuup](https://github.com/jonijuup)! - Four additions to `Nav` and `Tabs`, found by building a real app's file tree and page tabs on them. All are additive; nothing changes unless you opt in, apart from the new `aria-current` attribute, a disabled-trigger fix, and one rename (last below). - - **`Nav`: a branch that is also a page.** The new `Nav.Toggle` part is a caret for inside `Nav.Trigger`. Mounting it makes pressing the row (click, Enter or Space) activate it like `Nav.Item`, so its `onClick` or link routes, while the caret opens and closes the group. The caret is a real button that carries `aria-expanded` for the group, so name it with `aria-label`; like `Nav.Action` it stays out of the arrow-key order, where ArrowRight and ArrowLeft still open and close the group from the row. A press on the caret never follows a link row, and the caret respects a disabled trigger. Space now follows a link row as Enter does, on `Nav.Item` too. - - A disabled `Nav.Trigger` now also prevents the click's default action, as a disabled `Nav.Item` always has — so one rendered as a link no longer navigates while disabled. - - **`Nav`: `aria-current="page"` on active rows.** `active` on `Nav.Item` and `Nav.Trigger` previously produced only `data-active`, which assistive tech cannot see. Your own `aria-current` still wins. - - **`Tabs`: `openOnHover`.** `Tabs.Trigger` gains `openOnHover`, `openDelay` (50ms) and `closeDelay` (50ms). Resting the mouse on a tab opens it, and leaving closes it again unless the mouse heads into the popup, which a prediction cone tracks. A press on the tab, or a press or focus inside the popup, keeps it open. Hover never takes over a tab someone opened with a press, and only a mouse hovers. - - **`Tabs`: change event details.** `onValueChange` and `onItemsChange` receive a second argument, `eventDetails`, with `reason`, `event`, `trigger`, `cancel()` and `isCanceled`. Cancelling stops the change landing, so an editor can cancel a close in `onItemsChange` and ask about unsaved work first. The selection that follows a close is reported but cannot be cancelled, since its tab is already gone. The reasons are `"trigger-press"`, `"trigger-hover"`, `"list-navigation"`, `"close-press"`, `"keyboard"`, `"escape-key"` and `"imperative-action"`. Callbacks that take one argument keep working. - - Escape from inside a floating `Tabs.Popup` now hands focus back to its tab, instead of dropping it on the page as the popup closes. - - **Breaking:** `event.preventBaseUIHandler()` is now `event.preventPrimitiveHandler()`, and `event.baseUIHandlerPrevented` is now `event.primitiveHandlerPrevented`. Call it from your own handler on a part to skip the part's built-in handler. Development warnings now start with `@intentface/chat:` and link to the composition guide. +- [Nav] New `Nav.Toggle`: a caret inside `Nav.Trigger`, so a branch row can also be a page. +- [Nav] Add `aria-current="page"` to active `Nav.Item` and `Nav.Trigger` rows. +- [Nav] Fix a disabled `Nav.Trigger` rendered as a link still navigating. +- [Nav] Fix Space not following a link row. +- [Tabs] New `openOnHover`, `openDelay` and `closeDelay` on `Tabs.Trigger`. +- [Tabs] Add `eventDetails` to `onValueChange` and `onItemsChange`, with `reason` and `cancel()`, so a close can be cancelled. +- [Tabs] Fix Escape from a floating `Tabs.Popup` dropping focus instead of returning it to its tab. +- [Breaking] `event.preventBaseUIHandler()` is now `event.preventPrimitiveHandler()`, and `event.baseUIHandlerPrevented` is now `event.primitiveHandlerPrevented`. ## 0.3.0 ### Minor Changes -- [#78](https://github.com/Intentface/intentface-chat/pull/78) [`5872ad5`](https://github.com/Intentface/intentface-chat/commit/5872ad57ee8e84d4e9bcf77cc55c40dbb5238600) Thanks [@rpvilo](https://github.com/rpvilo)! - Add three app-shell primitives alongside the chat ones: `Shell` (a collapsible, - resizable sidebar with hover-peek), `Nav` (a nav tree with roving focus, - typeahead and a guide ladder) and `Tabs` (an open-ended, closable collection - whose panel renders in the layout or anchored to its own tab). Each ships on the - usual three-module layout, so a server component reaches every part. - - None of them persist anything or claim a global key: state goes out through - callbacks (`onOpenChange`, `onExpandedChange`, `onValueChange`, `onItemsChange`, - and the sidebar's `onResize`) and comes back in as `default*` props or, for the - sidebar's width, as the `--shell-sidebar-width` custom property. Where it is - kept, and which keystroke toggles what, stay the app's to decide. - - Also fixes the shared transition internals: `useAnimationsFinished` now runs its - callback when there is no element rather than returning silently, which - previously left a closing `Composer.Panel` or popover mounted for good, and - gained a `subtree` option. `useAnchorPositioning` publishes - `--anchor-available-width` and no longer animates a surface in from its - pre-placement position. +- [Shell] New primitive: a collapsible, resizable sidebar with hover-peek. +- [Nav] New primitive: a nav tree with roving focus, typeahead and a guide ladder. +- [Tabs] New primitive: closable tabs whose panel renders in the layout or anchored to its tab. +- [Composer] Fix a closing `Composer.Panel` staying mounted when there was nothing to animate. +- Fix anchored surfaces animating in from before they were placed, and add `--anchor-available-width`. ## 0.2.1 ### Patch Changes -- [#76](https://github.com/Intentface/intentface-chat/pull/76) [`fd5575b`](https://github.com/Intentface/intentface-chat/commit/fd5575bc745bf8dc6f91177670c1fded75e8b267) Thanks [@rpvilo](https://github.com/rpvilo)! - `Thread` no longer chases the live edge with a smooth scroll while the container around it is being resized. A consumer that animates the thread open — a collapsed pill springing to a panel, a drawer sliding in — saw the transcript land mid-viewport and then visibly scroll to the bottom over the length of the animation. It now lands at the bottom and stays there. - - The follow is driven by a `ResizeObserver` on the content column, which fires for two different things it could not tell apart: a token streaming in, and the viewport itself changing size. The second is not hypothetical for auto-scrolling threads — in every mode that lands at the top (`follow`, `jump`), the last turn reserves a viewport via `--thread-turn-min-height: var(--thread-turn-area)`, and `--thread-turn-area` is derived from the thread root's `clientHeight`. So a container animating its height rewrites that variable each frame, resizing the content column each frame, and each resize was answered with a fresh smooth scroll to a target that had already moved. - - `follow` now compares the scroll viewport's own box against the previous callback's. Streamed content never changes it; a container animating open or a window resize always does. A composer docked over the transcript is out of the scroller's flow, so growing it moves no box and is not covered here. A changed box still pins to the live end — it just does so instantly, which is the whole difference between landing at the bottom and animating toward it. Width is compared alongside height, so a container that expands horizontally and reflows the transcript is covered too. - - The intent-only gating is unchanged: the resize branch is a synchronous `clientWidth` / `clientHeight` read inside the observer callback, not the one-frame-stale at-bottom snapshot that `follow` deliberately avoids consulting. Threads in a static container are unaffected — the viewport box never changes, so every callback takes the existing smooth path. - - `autoScroll="bottom"` never exhibited this, since it is the one mode that sets no reserve. - -- [#76](https://github.com/Intentface/intentface-chat/pull/76) [`fd5575b`](https://github.com/Intentface/intentface-chat/commit/fd5575bc745bf8dc6f91177670c1fded75e8b267) Thanks [@rpvilo](https://github.com/rpvilo)! - `Thread` no longer loses a few pixels of scroll position when the composer changes height. Adding or discarding an attachment — anything that grows or shrinks the dock while you are pinned to the bottom — nudged the transcript down by a handful of pixels and never gave them back. Repeated often enough, the thread drifted away from the live edge. - - `useThreadInsets` derives two custom properties from one dock measurement, and they are designed to cancel: the content wrapper pads by `--thread-overlay-bottom-height`, and the same inset is subtracted from `--thread-turn-area`, which the last turn reserves. Their sum is constant, so the scrollable height should never move when the dock resizes. - - It moved anyway, because the write order broke the cancellation. The padding was written first, then `measureTopInset` and `root.clientHeight` were read — both force a synchronous layout. That layout ran with the _new_ padding against the _old_ reserve, so `scrollHeight` dipped for exactly one frame. The browser clamps `scrollTop` to fit shorter content, and clamping is not reversed when the content grows back a frame later, so each dock resize cost a few pixels permanently. - - Every measurement is now read before either property is written, so both land in the same recalculation and no intermediate layout exists to clamp against. Measured across an attachment discard: `--thread-turn-area` climbs 504px → 566px over 23 frames while `scrollTop`, the scroll maximum, and the last turn's on-screen position all hold still. Previously `scrollTop` dropped 5351 → 5345 on the third frame and stayed there. +- [Thread] Fix visibly scrolling to the bottom while the container animates open. +- [Thread] Fix losing a few pixels of scroll position when the composer changes height. ## 0.2.0 ### Minor Changes -- [#69](https://github.com/Intentface/intentface-chat/pull/69) [`0fb9b14`](https://github.com/Intentface/intentface-chat/commit/0fb9b1441158bc7a2270ac1e20565812aa4c972c) Thanks [@rpvilo](https://github.com/rpvilo)! - Disclosure panels (`Steps.Panel`, `Reasoning.Content`) no longer stay pinned to the height they had when they opened. The panel measures its natural height for the transition and then **releases** that measurement once the transition settles, so an open panel tracks content that appears underneath it — a nested disclosure expanding, rows streaming in. Previously the measurement was written on open and never cleared, so any consumer following the documented `height: var(…)` pattern had its content clipped at the open-time height. - - **Breaking:** the measured height is published as `--panel-height` instead of `--collapsible-panel-height`. The old name leaked an internal: `Collapsible` is not a public part, cannot be imported, and appears nowhere in the docs, so a consumer styling `Steps.Panel` had to reach for a variable named after a component they could not see. The variable is now documented on both parts. - - ```diff - [data-steps-panel] { - overflow: hidden; - - height: var(--collapsible-panel-height); - + height: var(--panel-height); - transition: height 150ms ease-out; - } - ``` - - Note the value is present only while the open or close transition runs, which is what makes the fallback to `auto` work while open. That is the intended contract, not a gap. - - Two supporting changes, both internal: - - - `useTransitionStatus` is now called with `enableIdleState` and `deferEndingState` enabled. `idle` is the settled-open status the release gates on, and deferring `ending` by a frame leaves one frame where a closing panel is still at its open size — which is where the close has to be measured. Previously the close was measured on the `ending` frame, with the closed styles already applied, so it measured the clamped box. - - The unmount is now gated on `transitionStatus === "ending"` rather than merely `!open`. With the ending state deferred, the earlier check could call `getAnimations()` before the closed styles landed, find nothing running, and cut the exit animation off. This also affects `Composer.Panel`, the other consumer of the shared transition hook. - - Measurement also neutralizes inline alignment properties for the read and restores them immediately, matching Base UI — inline alignment can distort a scroll-based measurement. - -- [#67](https://github.com/Intentface/intentface-chat/pull/67) [`414d132`](https://github.com/Intentface/intentface-chat/commit/414d132c335fd9678a2ef626d45d074ed9464660) Thanks [@rpvilo](https://github.com/rpvilo)! - **Breaking:** `Thread.Root` no longer takes `dockSelector`. The thread now measures the `Thread.Composer` slot (`[data-thread-composer]`) it already renders, instead of querying the composer's internals for a set of dock parts. Dock your composer in the slot and the measurement is automatic: - - ```diff - - - + - {turns} - -
{composer}
- + {composer} -
- ``` - - This is what the documentation already described — `THREAD.md` and the build-a-chat guide both said the thread measured `Thread.Composer`, while the code measured `[data-composer-context-window], [data-composer-container]` and only ever used the bottom-most match as the reference edge. The old default also meant `Composer.ContextWindow` was never reserved for: it sits above `Composer.Container`, so the container won the measurement and the context strip had to fit inside the 32px content gap or content slid underneath it. Docking the whole slot fixes that, because the slot's box covers every in-flow part. - - The rule is now positional rather than configured: anything inside `Thread.Composer` that should not push content up must be out of the slot's flow. `Thread.ScrollButton` and the default portaled `Composer.Panel` already are, so the standard composition is unaffected. An in-flow panel (`Composer.Panel anchor={false}`) now counts as part of the dock and the viewport insets around it, where previously it overlapped the last turn. +- [Steps] [Reasoning] Fix the panel clipping content that grows after it opens. +- [Steps] [Reasoning] [Breaking] The panel height variable `--collapsible-panel-height` is now `--panel-height`. +- [Thread] [Breaking] `Thread.Root` no longer takes `dockSelector`. Put the composer in `Thread.Composer` and it is measured automatically. ### Patch Changes -- [#68](https://github.com/Intentface/intentface-chat/pull/68) [`7be61d3`](https://github.com/Intentface/intentface-chat/commit/7be61d314defd876b06b5be66ab53e44a053dbcd) Thanks [@rpvilo](https://github.com/rpvilo)! - `exports` now points at `./dist` permanently, so the registry metadata matches the tarball. Previously `exports` pointed at `./src/*.ts` in the repo — so the docs app could consume package source with no build step — and a `prepack`/`postpack` pair swapped it to `./dist` for packing. npm builds the packument from `package.json` as it stands _after_ `postpack`, which restored the source paths, so every published version advertised `./src/*.ts` for all 11 subpaths: files the tarball does not ship. - - Consumers were never affected, because Node resolves against the `package.json` inside the tarball, which always carried the correct `./dist` paths. But `npm view @intentface/chat exports` reported paths that do not exist, which reads exactly like a broken publish — and npm was warning that `publishConfig.exports` "will stop working in the next major version of npm", so the mechanism had an expiry date regardless. - - The swap is gone: `scripts/swap-exports.mjs`, the `postpack` hook, and `publishConfig.exports` are all deleted, and `prepack` now just runs the build. The app gets source resolution from the repo instead of from the published exports map — a `paths` entry in `tsconfig.json` and a matching Turbopack `resolveAlias` in `next.config.ts`, both mapping the 11 subpaths to `packages/chat/src`. Verified by building the docs app with `packages/chat/dist` deleted entirely. - - No API change; nothing to migrate. - -- [#71](https://github.com/Intentface/intentface-chat/pull/71) [`9a378b5`](https://github.com/Intentface/intentface-chat/commit/9a378b579f5e11ad26f30441e1893dc8f8cec8eb) Thanks [@rpvilo](https://github.com/rpvilo)! - Point `homepage` and the README at `https://ui.intentface.com`. The previous address, `intentface.dev`, does not resolve — so the link on the npm page and the two documentation links inside the shipped README were dead. The documentation now also lives at the root of that host rather than under `/docs`, so the paths lose that prefix. - - No code change; published metadata only. - -- [#63](https://github.com/Intentface/intentface-chat/pull/63) [`d73bb3f`](https://github.com/Intentface/intentface-chat/commit/d73bb3f2b587616412a3a5ef09cc888a0eb8c2c1) Thanks [@rpvilo](https://github.com/rpvilo)! - Fix `Composer.Textarea` accumulating phantom newlines during rapid editing. The editor's padding `
` is now marked with `data-padding-break` and recognized structurally instead of being inferred from position, so a native edit that strands it mid-document no longer reads it back as real content. The padding also stops consuming a caret position, keeping the DOM's position space aligned with the model's length. +- [Composer] Fix `Composer.Textarea` gaining phantom newlines during rapid editing. +- Fix the npm registry listing `exports` paths the package doesn't ship. Installs were unaffected. +- Point `homepage` and the README links at https://ui.intentface.com. ## 0.1.2 ### Patch Changes -- [#65](https://github.com/Intentface/intentface-chat/pull/65) [`987e276`](https://github.com/Intentface/intentface-chat/commit/987e2766e08df78049bdd4575c35b5e05d16f6f7) Thanks [@valstu](https://github.com/valstu)! - Add the missing `.js` extensions to single-quoted specifiers in `dist`, which - made the package unloadable under SSR. - - `add-dist-extensions` matched only double-quoted specifiers. The vendored files - under `src/internal/render` came from Base UI's source and use single quotes, so - 21 of their relative specifiers (13 in `.js`, 8 in `.d.ts`) shipped - extensionless. Bundlers resolve those, which is why the browser was fine; Node's - ESM resolver requires fully-specified paths and threw - `Cannot find module .../internal/render/useMergedRefs` on the first import. Every - part goes through `useRenderElement`, so importing _any_ entry point on a server - failed — a Next or TanStack Start app rendered nothing server-side and silently - fell back to client rendering. - - The pattern now captures the quote character and backreferences it, so both - styles are rewritten and the quote is preserved. - - The build now ends with a smoke test that imports every subpath in - `publishConfig.exports` under Node — the entire consumer-reachable surface. The - failure was a resolution error the rewrite script could not see (its own pattern - was the blind spot) and publint does not resolve the internal graph, so the guard - tests resolution itself: against the previous rewrite, 8 of the 11 entries fail - to load; the next regression breaks the build instead of a consumer's server. +- Fix the package failing to load during server-side rendering. ## 0.1.1 ### Patch Changes -- [#58](https://github.com/Intentface/intentface-chat/pull/58) [`9f74772`](https://github.com/Intentface/intentface-chat/commit/9f747721d26299de9a15f7983e875ec9c8df3ad3) Thanks [@rpvilo](https://github.com/rpvilo)! - Fix two quadratic-backtracking regexes that could hang the browser. - - `parseChipSegments` is the serious one, because message text is untrusted — it - arrives from the model. The chip token pattern had two independent blowups. A long - run of `[` with no closing bracket made the label group consume to end-of-string, - fail, backtrack over every position, advance one character and repeat: 355ms at - 32k characters. Worse, `[a](chip:x:` repeated with no `)` anywhere did the same - through the value group from many start positions at once: 264ms at 88k - characters. Both grow quadratically, so a large message could hang a tab for - seconds. - - Every character class now excludes `[`, so no group can consume past the next - one and the work per start position is bounded by the gap to it. Safe for - anything `encodeChipMarkdown` produces — values are `encodeURIComponent`-escaped, - queries come from `URLSearchParams`, prefixes are short identifiers — and labels - containing raw brackets never parsed under the previous pattern either. - - `detectActivePrefix` had the same shape via `/\S*$/`, which retries from every - position when the text ends in whitespace. It runs on every keystroke, so pasting - a large single-token blob froze the editor — 16 seconds for a 200k-character run. - Replaced with a backwards index scan, which is linear, allocation-free, and - returns identical results. - - Both are covered by regression tests asserting a time budget that the previous - implementations exceeded by two orders of magnitude. +- Fix a regex in `parseChipSegments` that could hang the browser on long messages. +- [Composer] Fix a regex that could freeze the editor on large pasted text. ## 0.1.0 ### Minor Changes -- [#19](https://github.com/Intentface/intentface-chat/pull/19) [`563a527`](https://github.com/Intentface/intentface-chat/commit/563a5270f4bddedc1dd495da1af03925851c4d6d) Thanks [@rpvilo](https://github.com/rpvilo)! - Initial release: headless chat UI primitives for React, unstyled and animation-free. - - Eleven entry points. Each component entry exports a namespace whose parts you compose - yourself — `Composer.Root`, `Composer.Container`, `Message.Root`, `Message.Text` — rendering - semantic DOM with `data-*` state attributes, and every part takes a `render` prop for - swapping the underlying element: - - - `/composer` — rich-text input over a purpose-built contenteditable engine: inline chips, - `/` and `@` prefix-triggered command lists with fuzzy scoring, attachments, and the - ask-user questionnaire flow. One store per ``, or bring your own with - `Composer.createStore()` to drive it from outside the tree. - - `/thread` — scroll container with at-bottom detection, auto-follow, prepend-aware - restoration, and dock/overlay inset measurement. - - `/message` — message parts, turns, chip-segmented text, sources, actions, and selection. - - `/steps`, `/reasoning` — tool-call timelines and reasoning disclosure. - - `/chip`, `/attachments`, `/ask-user` — the remaining building blocks. - - `/types` — the structural message contract plus part type guards. - - `/message-utils` — part segmentation, turn grouping, and reasoning/source derivation. - - `/chip-markdown` — the self-describing chip wire format. - - Works with React Server Components. The package ships one module per part, so a server - component can render a static transcript directly — `Message.Root`, `Message.Text`, - `Chip.Root` and friends resolve across the client boundary rather than coming back - `undefined`. The parts remain client components, so hooks and event handlers behave as - usual; reach for `"use client"` where you need them. - - The types are framework-agnostic: an AI SDK `UIMessage` satisfies `ChatMessage` - structurally, so SDK messages pass straight into the utilities and components with no - adapter. Beyond the React 19 peer, the package pulls in only `@floating-ui/dom` (anchored - positioning) and `nanoid` (attachment ids) — no editor framework and no animation library. - - Styling is left entirely to the consumer; copy-paste styled sources and live previews live - at https://intentface.dev/docs. +- Initial release: headless chat primitives for React. `Composer`, `Thread`, `Message`, `Steps`, `Reasoning`, `Chip`, `Attachments` and `AskUser`, plus the message types and utilities. diff --git a/source.config.ts b/source.config.ts index 0a9897b..2597fd3 100644 --- a/source.config.ts +++ b/source.config.ts @@ -1,5 +1,6 @@ import { defineConfig, defineDocs, frontmatterSchema } from "fumadocs-mdx/config"; import { z } from "zod"; +import { remarkChangelogTags } from "./lib/docs/remark-changelog-tags"; export const docs = defineDocs({ dir: "content/docs", @@ -15,6 +16,7 @@ export const docs = defineDocs({ export default defineConfig({ mdxOptions: { + remarkPlugins: [remarkChangelogTags], // Disable fumadocs' built-in shiki pass so fenced code reaches our `pre` // handler as raw text. Highlighting is done in one place — the CodeBlock // component (fumadocs-core `highlight`) — shared by MDX fences, the styled