diff --git a/src/features/AGENTS.md b/src/features/AGENTS.md
index cadaf3e..fc9cf78 100644
--- a/src/features/AGENTS.md
+++ b/src/features/AGENTS.md
@@ -18,6 +18,7 @@
- **Tables** (slice 19, `chat/rich-text.tsx`; the parser in `src/lib/markdown.ts`, which only the browser uses): opencode's at 50c5fce (`docs/decisions.md`, Design, 2026-10-20): a real `
`, its header's named cells `
` in medium weight over `border-border-strong`, rows over `border-border`, both `0.5px` (Chromium draws 1px on a screen of 1x), cells `p-3 align-top`. Each cell is `min-w-36`, which Chromium honours on table cells, so a wide table scrolls rather than squeezing its columns to a word per line. Every row is padded to the widest row or the header with empty `
`s, so the hairlines run the full width; a header's empty name is a `
` too, as it names nothing, and a row or header of nothing but empty cells is dropped by the parser. The table sits in `SideScroll`, an `overflow-x-auto` box that a `ResizeObserver` measures, on itself and on the table: while the table is wider, the box is `role="region"`, named "Table, scrolls sideways", with `tabIndex={0}`, so the keyboard reaches it (axe's `scrollable-region-focusable`, which checks only a box that overflows); a table that fits adds no tab stop. A hidden Browser pane delivers the observer's first measure only when it next draws. The cells' runs are built in `RichText`'s render, not inside `SideScroll`, so the fade's character count (`words.at`) keeps the rendered order. The fake model writes no table, so `tests/e2e/chat.spec.ts` opens a kept conversation that holds two.
- **Stop** (slice 12): while the assistant works, the message box's Send button is Stop, one `
-
- This is a demo with fictional interviews. Don't enter personal
- information.
-
+
+
+
+ {site.model.name}
+ {" "}
+ · A free model: it can make mistakes, and follows short, specific
+ requests best.
+
+
+ This is a demo with fictional interviews. Don't enter personal
+ information.
+
+
diff --git a/src/lib/site.ts b/src/lib/site.ts
index 147f072..e5bf49c 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -7,6 +7,15 @@ export const site = {
links: {
repository: "https://github.com/TechNomadCode/prd-assistant",
},
+ /**
+ * The chat model as visitors see it under the message box: a name that
+ * says what it is, linked to the page of the exact model in
+ * `src/server/ai/models.ts`.
+ */
+ model: {
+ name: "OpenAI open model",
+ page: "https://developers.cloudflare.com/workers-ai/models/gpt-oss-120b/",
+ },
} as const;
/**
diff --git a/tests/e2e/chat.spec.ts b/tests/e2e/chat.spec.ts
index 9f163df..fb7b500 100644
--- a/tests/e2e/chat.spec.ts
+++ b/tests/e2e/chat.spec.ts
@@ -643,6 +643,24 @@ test.describe("chat", () => {
).toBeVisible();
});
+ test("names the model under the message box and says it can make mistakes", async ({
+ page,
+ }) => {
+ await openChat(page);
+
+ await expect(
+ page.getByRole("link", { name: "OpenAI open model", exact: true }),
+ ).toHaveAttribute(
+ "href",
+ "https://developers.cloudflare.com/workers-ai/models/gpt-oss-120b/",
+ );
+ await expect(
+ page.getByText(
+ "A free model: it can make mistakes, and follows short, specific requests best.",
+ ),
+ ).toBeVisible();
+ });
+
test("marks an answer that breaks off as unfinished, and trying again replaces it", async ({
page,
}, testInfo) => {
diff --git a/tests/unit/providers.test.ts b/tests/unit/providers.test.ts
index 2c7553f..63cb04c 100644
--- a/tests/unit/providers.test.ts
+++ b/tests/unit/providers.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
+import { site } from "@/lib/site";
import { FAKE_CHAT_MODEL } from "@/server/ai/fake-chat-model";
import { CHAT_MODEL, EMBEDDING_MODEL } from "@/server/ai/models";
import { createChatModel, createEmbedder } from "@/server/ai/providers";
@@ -91,6 +92,13 @@ describe("choosing the chat model", () => {
expect(createChatModel(cloudflare).modelId).toBe("@cf/openai/gpt-oss-120b");
});
+ it("links the model name visitors see to the page of the model the chat runs on", () => {
+ const slug = CHAT_MODEL.split("/").at(-1);
+ expect(site.model.page).toBe(
+ `https://developers.cloudflare.com/workers-ai/models/${slug}/`,
+ );
+ });
+
it.each([["mistral"], ["openai"], ["Fake"], [undefined]])(
"rejects an unknown adapter name: %s",
(provider) => {