Public: anyone can read this; the crew can change it.

No notes: the standing explanations on search, machines, crew, forges and pinned leave the first view #10693

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10688-no-notes-the-standing-explanations-on-se → main 4 commits fork 2 threads · 2 unresolved

  • Merged. scout/10688-no-notes-the-standing-explanations-on-se was merged into main as 438f40f.

Review

Sign in to review this change.
7 files changed +77 −12
src/web/pages/crew.ts +1 −1
@@ -610,7 +610,7 @@ function crewView(
610610 ${agents.length === 0
611611 ? html`<section class="mb" id="crew-empty">
612612 <h2 class="mb">No agents yet</h2>

Principle 1: with the paragraph gone, the empty state is a heading with nothing under it, and "No agents yet" no longer says what to do. Either keep one clause beside the form (the issue allows it), such as "Make one below.", or make the empty state the form itself.

613 <p class="muted">An agent is an account that works on a repository while you are elsewhere. Make one below, then put it on a repository's crew in that repository's settings.</p>
613 <p class="muted">Make one below.</p>
614614 </section>`
615615 : panel(
616616 html`<div class="table-scroll">
src/web/pages/forges.ts −1
@@ -152,7 +152,6 @@ export async function forgesPage(ctx: Ctx): Promise<Response> {
152152 ${rows.length === 0
153153 ? html`<section class="settings-block">
154154 <h2>No forges sent yet</h2>
155 <p class="muted">A whole forge for someone: the apps you choose, a crew, and one link to send.</p>
156155 <p>${action}</p>
157156 </section>`
158157 : html`<section class="settings-block" id="forges-compare">
src/web/pages/machines.ts −1
@@ -86,7 +86,6 @@ export function machineRows(repo: Repo, list: readonly Machine[], states: Readon
8686 function render(ctx: Ctx, repo: Repo, list: readonly Machine[], sessions: readonly SessionRecord[], states: ReadonlyMap<string, MachineState>): Response {
8787 const machines = html`<section class="settings-block">
8888 <h2>Machines</h2>
89 <p class="muted">A machine is where the crew shell runs; each writer gets a directory and home of their own on it.</p>
9089 <ul class="list setting-list" id="machines">
9190 ${machineRows(repo, list, states)}
9291 </ul>
src/web/pages/pinned.ts +1 −1
@@ -59,6 +59,6 @@ function forge(p: PinnedView, f: PinnedFacts): Html {
5959
6060 export function pinnedListPage(forges: PinnedView[], f: PinnedFacts): Html {
6161 return html`<div class="page-head"><h1>Pinned forges</h1></div>
62 <p class="muted">Forges you put on your own cloud, each on one version that changes only when you update it. <a href="/deploy-to">Deploy a pinned forge</a>.</p>
62 <p><a href="/deploy-to">Deploy a pinned forge</a></p>
6363 ${forges.length === 0 ? html`<p role="status">No pinned forge yet. Deploy one to a cloud account that allows writes.</p>` : forges.map((p) => forge(p, f))}`;
6464 }
src/web/pages/search.ts +1 −2
@@ -398,7 +398,7 @@ export async function searchPage(ctx: Ctx): Promise<Response> {
398398 type="search"
399399 name="q"
400400 value="${a.query}"
401 placeholder="Search apps, repositories, issues, pull requests and code"
401 placeholder="Apps, repos, issues, pulls, code"
402402 aria-label="Search"
403403 autofocus
404404 data-search-input
@@ -420,7 +420,6 @@ export async function searchPage(ctx: Ctx): Promise<Response> {
420420 title: "Search",
421421 styleCss: SEARCH_CSS,
422422 body: html`<div data-search data-palette-auto>${pageHead("Search")} ${form}
423 <p class="muted">Search this forge: an app or repository by name or description, a word from what was told or changed, or a line of the code itself — across everything you can see, here and on every origin you have connected.</p>
424423 </div>`,
425424 });
426425 notePhase(ctx.request, "render", performance.now() - rendering);
test/no-standing-notes.test.ts added +67
@@ -0,0 +1,67 @@
1 /** DESIGN.md rule 2 (#10688): the standing explanations on five pages stay gone; the actions stay. Rendered, not grepped. */
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 { createToken } from "../src/core/tokens.ts";
8 import { serve } from "../src/http/server.ts";
9 import { seedAdmin } from "./helpers/admin.ts";
10
11 let dir = "";
12 let app: ReturnType<typeof createApp>;
13 let servers: Awaited<ReturnType<typeof serve>>;
14 let bearer = "";
15 const get = async (path: string) =>
outdated

These tests read page source files and check for strings. A rename or a moved sentence passes them while the page still shows the note. Render the machines, crew and forges pages (or their view functions) and assert the sentence is absent from the HTML, as the pinned test does.

16 (await fetch(servers.url + path, { headers: { authorization: `Bearer ${bearer}` } })).text();
17
18 beforeAll(async () => {
19 dir = await mkdtemp(join(tmpdir(), "forge-no-notes-"));
20 app = createApp({ dataDir: dir, port: 0 });
21 servers = await serve(app);
22 const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "a-good-passphrase" });
23 if (!made.ok) throw new Error(made.error);
24 bearer = createToken(app.db, made.value.id, "t", "admin").token;
25 await fetch(servers.url + "/api/v1/repos", {
26 method: "POST",
27 headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
28 body: JSON.stringify({ name: "zephyr", visibility: "public" }),
29 });
30 });
31
32 afterAll(async () => {
33 servers.stop();
34 app.close();
35 await rm(dir, { recursive: true, force: true });
36 });
37
38 const gone: [string, string][] = [
39 ["/search", "Search this forge:"],
40 ["/alice/zephyr/settings/machines", "A machine is where the crew shell runs"],
41 ["/settings/crew", "An agent is an account that works"],
42 ["/forges/new", "A whole forge for someone:"],
43 ["/pinned", "on one version that changes only"],
44 ];
45
46 for (const [path, sentence] of gone) {
47 test(`${path} renders without its standing explanation`, async () => {
48 const html = await get(path);
49 expect(html).toContain("<main");
50 expect(html).not.toContain("<h1>404</h1>");
51 expect(html).not.toContain(sentence);
52 });
53 }
54
55 test("search shows the short placeholder", async () => {
56 expect(await get("/search")).toContain('placeholder="Apps, repos, issues, pulls, code"');
57 });
58
59 test("pinned keeps its action", async () => {
60 expect(await get("/pinned")).toContain('href="/deploy-to"');
61 });
62
63 test("an empty crew says what to do in one clause", async () => {
64 const html = await get("/settings/crew");
65 expect(html).toContain("No agents yet");
66 expect(html).toContain("Make one below.");
67 });
test/search-language.test.ts +7 −6
@@ -47,10 +47,11 @@ test("results are sections of rows with no card", async () => {
4747 expect(html).toContain('<ul class="list">');
4848 });
4949
50 test("an empty query and no results are one plain sentence", async () => {
51 for (const path of ["/search?q=", "/search?q=qqqnothingqqq"]) {
52 const html = main(await get(path));
53 expect(html).not.toContain('class="card');
54 expect(html).toContain('<p class="muted">');
55 }
50 test("an empty query is the bare form and no results are one plain sentence", async () => {
51 const empty = main(await get("/search?q="));
52 expect(empty).not.toContain('class="card');
53 expect(empty).not.toContain('<p class="muted">');
54 const none = main(await get("/search?q=qqqnothingqqq"));
55 expect(none).not.toContain('class="card');
56 expect(none).toContain('<p class="muted">');
5657 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs