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) => {