Public: anyone can read this; the crew can change it.
The /agents page, /llms.txt as a page for assistants, speaks the home page's language #9893
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9873-the-agents-page-llms-txt-as-a-page-for-a → main 2 commits fork
2 files
changed +47
−6
src/web/pages/agents.ts +8 −6
| @@ -10,7 +10,7 @@ import type { Ctx } from "../../http/context.ts"; | ||
| 10 | 10 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; |
| 11 | 11 | import { pageHead } from "../chrome.ts"; |
| 12 | 12 | import { html } from "../html.ts"; |
| 13 | import { btn, card, field as formField } from "../ui.ts"; | |
| 13 | import { btn, field as formField } from "../ui.ts"; | |
| 14 | 14 | import { page } from "../layout.ts"; |
| 15 | 15 | import { renderMarkdown } from "../markdown.ts"; |
| 16 | 16 | import { llmsBody } from "../standards.ts"; |
| @@ -51,12 +51,14 @@ export function agentsPage(ctx: Ctx): Response { | ||
| 51 | 51 | sub: html`What this forge is, and the steps an assistant can take for you, or tell you to take. The same |
| 52 | 52 | words as plain text: <a href="/llms.txt">/llms.txt</a>.`, |
| 53 | 53 | })} |
| 54 | ${card(html`<form method="post" action="/forges/new" class="stack" id="agent-forge"> | |
| 55 | <h2>Get a forge of your own</h2> | |
| 54 | <section class="stack" aria-labelledby="agent-forge-h"> | |
| 55 | <h2 id="agent-forge-h">Get a forge of your own</h2> | |
| 56 | 56 | <p class="muted small">Free within limits while hosting is not sold. It sleeps when idle and wakes on its next visit; bring your own model credential or its crew stays off.</p> |
| 57 | ${formField({ label: "Name", name: "name", required: true })} | |
| 58 | ${btn("Make my forge", { variant: "primary" })} | |
| 59 | </form>`)} | |
| 57 | <form method="post" action="/forges/new" class="stack" id="agent-forge"> | |
| 58 | ${formField({ label: "Name", name: "name", required: true })} | |
| 59 | ${btn("Make my forge", { variant: "primary" })} | |
| 60 | </form> | |
| 61 | </section> | |
| 60 | 62 | <article class="prose prose-page" id="agents-guide">${renderMarkdown(linkAddresses(withoutTitle(body)))}</article>`, |
| 61 | 63 | })); |
| 62 | 64 | } |
test/agents-page-language.test.ts added +39
| @@ -0,0 +1,39 @@ | ||
| 1 | /** /agents speaks the home page's language (#9873): a section, no card, the words of /llms.txt. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { serve } from "../src/http/server.ts"; | |
| 8 | ||
| 9 | let dir = ""; | |
| 10 | let app: ReturnType<typeof createApp>; | |
| 11 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 12 | ||
| 13 | beforeAll(async () => { | |
| 14 | dir = await mkdtemp(join(tmpdir(), "forge-agents-lang-")); | |
| 15 | app = createApp({ dataDir: dir, port: 0 }); | |
| 16 | servers = await serve(app); | |
| 17 | }); | |
| 18 | afterAll(async () => { | |
| 19 | servers.stop(); | |
| 20 | app.close(); | |
| 21 | await rm(dir, { recursive: true, force: true }); | |
| 22 | }); | |
| 23 | ||
| 24 | test("/agents has no card, keeps the form, and says what /llms.txt says", async () => { | |
| 25 | const res = await fetch(servers.url + "/agents"); | |
| 26 | expect(res.status).toBe(200); | |
| 27 | const text = await res.text(); | |
| 28 | const main = text.slice(text.indexOf("<main"), text.indexOf("</main>")); | |
| 29 | expect(main).not.toContain('class="card'); | |
| 30 | expect(main).not.toContain("card-body"); | |
| 31 | expect(main).toContain('<section class="stack" aria-labelledby="agent-forge-h">'); | |
| 32 | expect(main).toMatch(/<form method="post" action="\/forges\/new"/); | |
| 33 | expect(main).toContain("<h2 id=\"agent-forge-h\">Get a forge of your own</h2>"); | |
| 34 | const llms = await (await fetch(servers.url + "/llms.txt")).text(); | |
| 35 | // Plain prose lines (letters, spaces, commas, periods) must appear verbatim on the page. | |
| 36 | const prose = llms.split("\n").map((l) => l.replace(/^[-*] /, "")).filter((l) => /^[A-Z][A-Za-z ,.]{20,}$/.test(l)); | |
| 37 | expect(prose.length).toBeGreaterThan(0); | |
| 38 | for (const line of prose) expect(main).toContain(line); | |
| 39 | }); |