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

A repository's runs list and a run's log page speak the home page's language #9739

Merged opened this

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

Merged

scout/forge:scout/9727-a-repository-s-runs-list-and-a-run-s-log → main 4 commits fork 2 threads · 2 unresolved

  • Merged. scout/9727-a-repository-s-runs-list-and-a-run-s-log was merged into main as b32a9a7.

Review

Sign in to review this change.
7 files changed +46 −4
docs/design/areas/runs.md +15
@@ -14,3 +14,18 @@ own bounds. No fixed width; the page does not scroll sideways. No new class.
1414 See it on a phone: `/preview-admin/sample/runs/<id>` renders a sample run's
1515 page; at 390 px the head and live region wrap in place. That is asserted from
1616 the HTML and CSS (`test/run-page-fit.test.ts`), not from a 390 px render.
17
18 ## Rows and space, not boxes (#9727)
19
20 The runs list (`runsListPage`, `src/web/studio/lists.ts`) is a `.card.bare` list: one row per run, its state a
21 badge, its agent and age in the row's meta, on the page's one background with no frame. A run's log (`.run-log`)
22 has no box either: no fill, border or radius, just space around its rows, still wrapping and scrolling inside its
23 own bounds; the steer form under it keeps one hairline above it. Filter, working tab, live tail, refresh, cancel and
24 the `/runs/:id` door are unchanged. No new class.
25
26 The app page's log tail (`.run-log.log-tail`, a peek at a running row's log) shares `.run-log`, so it loses its
27 frame too. That is intended: a peek under a row reads as part of the row, not a box inside it.
28
29 A check run's step list (`#check-steps`) is a `.card.bare` list too, so `/runs/:id` has no boxed card. At 390 px a
30 log row's only fixed width is its 72px speaker column; the words (`.row-main`, `min-width: 0`) take the rest and
31 wrap (`test/run-page-fit.test.ts`).
src/web/studio/lists.ts +1
@@ -870,6 +870,7 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> {
870870 trail: wordBadge(info.word),
871871 });
872872 }),
873 { extra: "bare" },

Principle 7 (rows over boxes) is met here, but no test would fail without { extra: "bare" }. Add a test in test/runs-*.test.ts asserting the runs list renders card bare.

873874 );
874875 const last = fetched[fetched.length - 1];
875876
src/web/studio/run.ts +1
@@ -307,6 +307,7 @@ export async function runView(
307307 }),
308308 }),
309309 ),
310 { extra: "bare" },
310311 )}
311312 ${known
312313 ? html`<progress aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>`
src/web/style/content.ts +3 −4
@@ -676,11 +676,10 @@ form.inline { display: inline; margin-left: auto; }
676676 head stays reachable, and a composer that sits under it. */
677677 #run-live { position: relative; }
678678 .run-log {
679 margin: 0; padding: 10px 14px 8px; max-height: calc(100dvh - 300px); min-height: 12em; overflow: auto;
680 background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius);
679 margin: 0; padding: 8px 0; max-height: calc(100dvh - 300px); min-height: 12em; overflow: auto;
680 background: none; border: 0; border-radius: 0;
681681 font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; tab-size: 4;
682682 }

Principle 6/7: the log is unframed, but the check step list on the same page (run.ts:299 listCard() is still a box. Make it .card.bare so the page follows through.

683 #run-live:has(#run-steer) .run-log { border-radius: var(--radius) var(--radius) 0 0; }
684683 .run-log .row { padding: 2px 0; min-height: 0; align-items: flex-start; }
685684 .run-log .row-lead { flex: 0 0 72px; justify-content: flex-start; overflow: hidden; }
686685 .run-log .row-lead .badge { overflow: hidden; text-overflow: ellipsis; }
@@ -697,7 +696,7 @@ form.inline { display: inline; margin-left: auto; }
697696 }
698697 #run-steer {
699698 position: sticky; bottom: 0; gap: 6px; padding: 10px 12px; background: var(--bg);
700 border-top: 0; border-radius: 0 0 var(--radius) var(--radius); box-shadow: none;
699 border: 0; border-top: 1px solid var(--border); border-radius: 0; box-shadow: none;
701700 }
702701 #run-steer textarea { min-height: 38px; max-height: 160px; resize: none; }
703702 #run-steer .bar .muted { flex: 1 1 200px; }
test/run-page-fit.test.ts +16
@@ -95,6 +95,22 @@ describe("a crew run's page at 390 px", () => {
9595 const rule = content.match(/\.run-log \{[^}]*\}/)?.[0] ?? "";
9696 expect(rule).toMatch(/overflow-wrap:\s*anywhere/);
9797 expect(rule).toMatch(/overflow:\s*auto/);
98 // Space, not a frame (#9727).
99 expect(rule).toMatch(/border:\s*0/);
100 expect(rule).toMatch(/background:\s*none/);
98101 expect(content).toMatch(/\.run-log \.row-title \{[^}]*white-space:\s*pre-wrap/);
99102 });
103
104 test("with the frame gone, a log row fixes no width beyond its 72px speaker column (#9727)", () => {
105 const rule = (sel: string) =>
106 [...content.matchAll(/([^{}]+)\{([^}]*)\}/g)].find((m) => m[1]!.trim() === sel)?.[2] ?? "";
107 // The log has no side padding and no border: its rows get the page's whole width.
108 expect(rule(".run-log")).toMatch(/padding:\s*8px 0;/);
109 // The speaker column is the only fixed width: 72px, well under 390.
110 expect(rule(".run-log .row-lead")).toMatch(/flex:\s*0 0 72px/);
111 // The words take what is left and may shrink below their content, wrapping in place.
112 expect(components).toMatch(/\.row-main \{[^}]*flex:\s*1;[^}]*min-width:\s*0/);
113 expect(rule(".run-log .row-title")).toMatch(/white-space:\s*pre-wrap/);
114 expect(rule(".run-log .row-title")).not.toMatch(/nowrap|min-width/);
115 });
100116 });
test/runs-state-filter.test.ts +6
@@ -76,3 +76,9 @@ test("a private repository is still the one 404 to an outsider", async () => {
7676 expect(a.status).toBe(404);
7777 expect(b.status).toBe(404);
7878 });
79
80 test("the runs list is bare rows, not a boxed card (#9727)", async () => {
81 const { body } = await get("/alice/notes/runs?state=failed");
82 expect(body).toMatch(/<div\s+class="card bare"[^>]*>\s*<ul class="list">/);
83 expect(body).toContain(`/alice/notes/runs/${failedId}`);
84 });
test/studio-runs.test.ts +4
@@ -891,6 +891,10 @@ describe("working: a run's own page in the studio", () => {
891891 expect(live).toBeGreaterThan(-1);
892892 expect(page.indexOf('id="check-pins"')).toBeGreaterThan(page.indexOf("Test files"));
893893 expect(page.slice(live, page.indexOf('id="check-pins"'))).not.toContain("a breaks");
894 // The steps are bare rows, not a boxed card (#9727).
895 expect(page.slice(page.indexOf('id="check-steps"'), page.indexOf('id="check-pins"'))).toMatch(
896 /<div\s+class="card bare"[^>]*>\s*<ul class="list">/,
897 );
894898 // The replaced element (#check-steps) closes before the pin slot, which is
895899 // patched in place so focus and scroll survive a refetch.
896900 const at = page.indexOf('id="check-steps"');

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs